@zq-silk/yui 2.17.0 → 2.19.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 -1
- package/dist/commands/controllerCommands.js +14 -4
- package/dist/commands/durableJobCommands.js +50 -3
- package/dist/commands/globalRoleCommands.js +18 -12
- package/dist/commands/inboxCommands.js +1 -0
- package/dist/commands/operatorCommands.js +10 -2
- package/dist/controlPlaneCli.js +21 -4
- package/dist/controller/clientRuntime.js +34 -13
- package/dist/controller/controller.js +45 -22
- package/dist/controller/diagnosticJournal.js +214 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +143 -101
- package/dist/controller/globalInputDelivery.js +6 -2
- package/dist/controller/globalRuntimeAttention.js +11 -3
- package/dist/controller/jobControl.js +1 -1
- package/dist/controller/jobDefinitionControl.js +24 -13
- package/dist/controller/jobSupervisor.js +4 -0
- package/dist/controller/messageHealth.js +354 -0
- package/dist/controller/runtime.js +75 -5
- package/dist/controller/taskHostTerminalRecovery.js +11 -0
- package/dist/controller/workerSchedulerStore.js +24 -0
- package/dist/core/boundedRpc.js +4 -0
- package/dist/core/controllerServer.js +1 -1
- package/dist/core/controllerTelemetry.js +92 -19
- package/dist/job/globalShell.js +51 -0
- package/dist/job/jobBusinessResult.js +4 -3
- package/dist/job/jobDefinition.js +6 -1
- package/dist/job/jobRunner.js +9 -0
- package/dist/job/scheduleService.js +2 -0
- package/dist/message/globalInboxRecovery.js +30 -19
- package/dist/message/globalProviderRetry.js +7 -1
- package/dist/runtime/builtinAgentErrorMappers.js +4 -0
- package/dist/runtime/globalSessionHealth.js +39 -0
- package/dist/scheduler/roleRunStall.js +10 -10
- package/dist/storage/contextRecords.js +16 -0
- package/dist/storage/persistenceWorker.js +41 -19
- package/dist/storage/sqliteSchema.js +35 -11
- package/dist/storage/sqliteStore.js +102 -8
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +25 -11
- package/dist/storage/taskReadRevisionSchema.js +39 -0
- package/dist/web/assets/client/app/catalog.js +0 -29
- package/dist/web/assets/client/app/jobs.js +115 -0
- package/dist/web/assets/client/app/main.js +17 -6
- package/dist/web/assets/client/app/manage.js +1 -0
- package/dist/web/assets/client/app/settings.js +1 -5
- package/dist/web/assets/client/i18n/en.js +203 -18
- package/dist/web/assets/client/i18n/zh-CN.js +202 -17
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/layout/manageShell.js +61 -15
- package/dist/web/assets/client/layout/router.js +2 -1
- package/dist/web/assets/client/lib/prefs.js +1 -1
- package/dist/web/assets/client/views/jobs.js +1042 -160
- package/dist/web/assets/client/views/sidebar.js +3 -0
- package/dist/web/assets/shared/manageNav.js +3 -1
- package/dist/web/assets/shell/appRail.js +2 -1
- package/dist/web/assets/shell/manage.js +7 -2
- package/dist/web/assets/shell/models.js +4 -3
- package/dist/web/assets/shell/settingsContent.js +0 -3
- package/dist/web/assets/shell/sidebar.js +0 -1
- package/dist/web/assets/styles/views/jobs.js +301 -12
- package/dist/web/assets/styles/views/sidebar.js +1 -2
- package/dist/web/webServer.js +28 -1
- package/docs/global-shell-jobs.md +78 -0
- package/docs/job-runs.md +6 -3
- package/docs/managed-turn-and-session-runtime.md +170 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +116 -0
- package/docs/release-workflow.md +63 -0
- package/docs/release-workflow.zh-CN.md +44 -0
- package/docs/storage-baseline.md +14 -1
- package/docs/storage-baseline.zh-CN.md +6 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/package.json +1 -1
|
@@ -1696,7 +1696,8 @@ export const ROOT_COMMAND = buildNode({
|
|
|
1696
1696
|
: name === "run-delegation" ? "Read the exact execution Task/WorkItem and attention projection; never a grant."
|
|
1697
1697
|
: `${name} a stable Job or exact Run.`,
|
|
1698
1698
|
usage: `yui job ${name} (--task <id>|--global)${name === "list" ? "" : " --job <id>"}`
|
|
1699
|
-
+ (name === "define"
|
|
1699
|
+
+ (name === "define" ? " --title <text> (--input <json>|--input-file <file>|--command <shell>) [--script <path=local-file> ...]" : "")
|
|
1700
|
+
+ (name === "update" ? " [--title <text>] [--input <json>|--input-file <file>|--command <shell>] [--script <path=local-file> ...]" : "")
|
|
1700
1701
|
+ (name === "update" ? " --revision <current>" : "")
|
|
1701
1702
|
+ (name === "run" ? " --request-id <key> [--retry-of <run-id>]" : "")
|
|
1702
1703
|
+ (["run-get", "run-result", "run-evidence", "run-confirm", "run-delegation", "logs", "run-cancel", "run-acknowledge"].includes(name) ? " --run <run-id>" : "")
|
|
@@ -161,10 +161,9 @@ export function parseControllerRuntimeSnapshot(result, droppedEvents) {
|
|
|
161
161
|
const drain = runtime.drain;
|
|
162
162
|
const commands = runtime.commands;
|
|
163
163
|
const routes = commands?.routes;
|
|
164
|
-
const dispatcher = commands?.dispatcher;
|
|
165
164
|
const eventLoopDelay = commands?.eventLoopDelay;
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
// Dispatcher work is a subset of authenticated routes, not another queue.
|
|
166
|
+
const queueDepth = typeof routes?.inFlight === "number" ? routes.inFlight : undefined;
|
|
168
167
|
return {
|
|
169
168
|
source: "controller.status",
|
|
170
169
|
pid: typeof record.pid === "number" ? record.pid : undefined,
|
|
@@ -182,6 +181,17 @@ export function parseControllerRuntimeSnapshot(result, droppedEvents) {
|
|
|
182
181
|
? drain.progressEventsCoalesced
|
|
183
182
|
: undefined,
|
|
184
183
|
droppedEvents,
|
|
184
|
+
observations: {
|
|
185
|
+
commands: (runtime.commands ?? null),
|
|
186
|
+
phases: (runtime.phases ?? null),
|
|
187
|
+
reads: (runtime.reads ?? null),
|
|
188
|
+
persistence: (runtime.persistence ?? null),
|
|
189
|
+
diagnostics: (runtime.diagnostics ?? null),
|
|
190
|
+
messageHealth: (runtime.messageHealth ?? null),
|
|
191
|
+
scheduler: (runtime.scheduler ?? null),
|
|
192
|
+
preRoutingWait: "unmeasured; event-loop lag is sampled, not per-request queue time",
|
|
193
|
+
clock: "process-monotonic milliseconds; target/request labels are sha256 prefixes"
|
|
194
|
+
},
|
|
185
195
|
queueDepth,
|
|
186
196
|
oldestInFlightAgeMs: typeof routes?.oldestInFlightAgeMs === "number"
|
|
187
197
|
? routes.oldestInFlightAgeMs
|
|
@@ -210,7 +220,7 @@ export function renderRuntimeIdentitySection(input) {
|
|
|
210
220
|
}
|
|
211
221
|
lines.push(` Health ${health.status === "ok" ? "ok" : health.status === "degraded" ? "DEGRADED (needs-repair above)" : "FAIL (contradictions above)"}`, ` Controller ${runtime.source === "controller.status" && runtime.pid !== undefined
|
|
212
222
|
? `PID ${runtime.pid}${runtime.uptimeMs === undefined ? "" : `, up ${formatDuration(runtime.uptimeMs)}`}${runtime.protocolVersion === undefined ? "" : `, protocol ${runtime.protocolVersion}`}`
|
|
213
|
-
: "not running (unsupported)"}`, ` Controller RSS ${runtime.rssBytes !== undefined ? formatBytes(runtime.rssBytes) : UNSUPPORTED} (inventory ${formatBytes(inventoryRssBytes)})`, `
|
|
223
|
+
: "not running (unsupported)"}`, ` Controller RSS ${runtime.rssBytes !== undefined ? formatBytes(runtime.rssBytes) : UNSUPPORTED} (inventory ${formatBytes(inventoryRssBytes)})`, ` Commands in flight ${runtime.queueDepth ?? UNSUPPORTED} · oldest ${runtime.oldestInFlightAgeMs === undefined
|
|
214
224
|
? UNSUPPORTED
|
|
215
225
|
: runtime.oldestInFlightAgeMs === null
|
|
216
226
|
? "—"
|
|
@@ -4,6 +4,7 @@ import { callFileTaskController } from "../controller/clientRuntime.js";
|
|
|
4
4
|
import { acknowledgeDurableJob, cancelDurableJob, getDurableJob, startDurableJob } from "../controller/jobClient.js";
|
|
5
5
|
import { resolveJobCaller, taskLocalActor } from "../task/taskAuthority.js";
|
|
6
6
|
import { boundedJobArtifactPayload } from "../artifacts/jobArtifactTransport.js";
|
|
7
|
+
import { readFileSync, statSync } from "node:fs";
|
|
7
8
|
/**
|
|
8
9
|
* The textual `--owner` forms accepted by `job start`. The public help text in
|
|
9
10
|
* the command catalog must document exactly these forms.
|
|
@@ -38,9 +39,25 @@ export async function runDurableJobCommand(args, options) {
|
|
|
38
39
|
}
|
|
39
40
|
async function definitionCommand(command, args, options) {
|
|
40
41
|
const flags = new Map();
|
|
42
|
+
const scripts = [];
|
|
43
|
+
const readText = (path) => {
|
|
44
|
+
if (!statSync(path).isFile() || statSync(path).size > 512 * 1024)
|
|
45
|
+
throw usageError("Job input files must be text files up to 512 KiB.");
|
|
46
|
+
return readFileSync(path, "utf8");
|
|
47
|
+
};
|
|
41
48
|
for (let i = 0; i < args.length; i++) {
|
|
42
49
|
const flag = args[i];
|
|
43
|
-
if (
|
|
50
|
+
if (flag === "--script") {
|
|
51
|
+
const value = args[++i] ?? "", separator = value.indexOf("=");
|
|
52
|
+
if (separator < 1 || separator === value.length - 1)
|
|
53
|
+
throw usageError("--script requires relative-path=local-file.");
|
|
54
|
+
const path = value.slice(0, separator);
|
|
55
|
+
if (scripts.some(file => file.path === path))
|
|
56
|
+
throw usageError("Repeated script path.");
|
|
57
|
+
scripts.push({ path, content: readText(value.slice(separator + 1)) });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!["--global", "--task", "--job", "--title", "--input", "--input-file", "--command", "--revision", "--request-id", "--retry-of", "--run", "--artifact", "--reference", "--offset", "--result", "--expected-session", "--expected-attempt"].includes(flag)
|
|
44
61
|
|| flags.has(flag))
|
|
45
62
|
throw usageError(`Unknown or repeated Job flag: ${flag}.`);
|
|
46
63
|
if (flag === "--global") {
|
|
@@ -61,14 +78,44 @@ async function definitionCommand(command, args, options) {
|
|
|
61
78
|
?? (executionRead ? options.environment?.YUI_TASK_ID : undefined) ?? "");
|
|
62
79
|
const scope = taskId === undefined ? { kind: "global" } : { kind: "task", taskId };
|
|
63
80
|
let configuration;
|
|
64
|
-
if (flags.has("--input"))
|
|
81
|
+
if (flags.has("--input") && flags.has("--input-file"))
|
|
82
|
+
throw usageError("Choose --input or --input-file.");
|
|
83
|
+
if (flags.has("--input") || flags.has("--input-file")) {
|
|
65
84
|
try {
|
|
66
|
-
configuration = JSON.parse(flags.get("--input"));
|
|
85
|
+
configuration = JSON.parse(flags.has("--input-file") ? readText(flags.get("--input-file")) : flags.get("--input"));
|
|
67
86
|
}
|
|
68
87
|
catch {
|
|
69
88
|
throw usageError("--input must be a JSON Job configuration.");
|
|
70
89
|
}
|
|
71
90
|
}
|
|
91
|
+
if ((flags.has("--command") || scripts.length || flags.has("--input-file")) && !["define", "update"].includes(command)) {
|
|
92
|
+
throw usageError("Command and script edits require job define or update.");
|
|
93
|
+
}
|
|
94
|
+
if (flags.has("--command") || scripts.length) {
|
|
95
|
+
if (taskId !== undefined || configuration !== undefined)
|
|
96
|
+
throw usageError("--command/--script are global Shell shortcuts; use --input for a complete configuration.");
|
|
97
|
+
let shell = { kind: "shell", env: {}, steps: [], files: [] };
|
|
98
|
+
if (command === "update") {
|
|
99
|
+
const existing = await callFileTaskController(options.home, "job.definition", {
|
|
100
|
+
op: "show", scope, caller, id: flags.get("--job")
|
|
101
|
+
}, { environment: options.environment });
|
|
102
|
+
if (existing.revision !== Number(flags.get("--revision")))
|
|
103
|
+
throw usageError("Job revision changed; inspect before editing.");
|
|
104
|
+
if (existing.configuration.kind !== "shell")
|
|
105
|
+
throw usageError("Changing executor requires a new Job.");
|
|
106
|
+
shell = existing.configuration;
|
|
107
|
+
if (!flags.has("--title"))
|
|
108
|
+
flags.set("--title", existing.title);
|
|
109
|
+
}
|
|
110
|
+
if (flags.has("--command")) {
|
|
111
|
+
if (shell.steps.length > 1)
|
|
112
|
+
throw usageError("Use --input-file to edit a multi-step Job without discarding steps.");
|
|
113
|
+
shell = { ...shell, steps: [{ ...shell.steps[0], name: shell.steps[0]?.name ?? "run", command: flags.get("--command") }] };
|
|
114
|
+
// The command shortcut intentionally selects shell execution, not stale argv.
|
|
115
|
+
delete shell.steps[0].argv;
|
|
116
|
+
}
|
|
117
|
+
configuration = { ...shell, files: [...shell.files.filter(file => !scripts.some(next => next.path === file.path)), ...scripts] };
|
|
118
|
+
}
|
|
72
119
|
const requestId = flags.get("--request-id");
|
|
73
120
|
const parseObject = (flag) => {
|
|
74
121
|
if (!flags.has(flag))
|
|
@@ -2,7 +2,8 @@ import { roleNotFound, usageError } from "../errors/cliError.js";
|
|
|
2
2
|
import { hasManagedIdentity } from "../runtime/managedIdentity.js";
|
|
3
3
|
import { requireManagedGlobalCaller } from "../runtime/managedCaller.js";
|
|
4
4
|
import { boundedDocument, recordPage, readOptions, readDigest, messageReceipt, summaryText, summaryMetadata } from "../output/boundedRead.js";
|
|
5
|
-
import { controlProviderRetry, providerRetryProjection,
|
|
5
|
+
import { controlProviderRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
|
|
6
|
+
import { globalSessionHealth } from "../runtime/globalSessionHealth.js";
|
|
6
7
|
import { pendingGlobalInboxMessages, inboxHandlingState, globalInboxDeliveryState } from "../message/inboxReceipt.js";
|
|
7
8
|
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
8
9
|
import { globalInputBacklog } from "../message/globalInputBacklog.js";
|
|
@@ -134,6 +135,7 @@ function roleContext(args, store, options) {
|
|
|
134
135
|
const effective = resolveEffectiveLaunch({ role, purpose: "execution" });
|
|
135
136
|
const sessions = store.getGlobalRoleSessionSet(name);
|
|
136
137
|
const session = sessions?.sessions[role.activeAgentId];
|
|
138
|
+
const health = globalSessionHealth(role, sessions);
|
|
137
139
|
const currentSkills = checkCurrentSkills(options.yuiHome ?? environment.YUI_HOME, roleSkillIds(session?.effective.context ?? role, role.name === "operator" ? "operator" : "global"));
|
|
138
140
|
// Native delivery and explicit Agent handling settle the same queued input
|
|
139
141
|
// through distinct receipts; a context read does neither.
|
|
@@ -164,7 +166,9 @@ function roleContext(args, store, options) {
|
|
|
164
166
|
agentId: binding.agentId,
|
|
165
167
|
adapterId: binding.adapterId,
|
|
166
168
|
workspace: role.workspace,
|
|
167
|
-
|
|
169
|
+
desiredRevision: health.desiredRevision,
|
|
170
|
+
effectiveRevision: health.effectiveRevision,
|
|
171
|
+
configurationDrift: health.configurationDrift,
|
|
168
172
|
...(session?.nativeSessionId === undefined
|
|
169
173
|
? {}
|
|
170
174
|
: { nativeSessionId: session.nativeSessionId })
|
|
@@ -180,7 +184,7 @@ function roleContext(args, store, options) {
|
|
|
180
184
|
authority: {
|
|
181
185
|
view: role.name === "operator" ? "operator" : "global",
|
|
182
186
|
taskImplementation: false,
|
|
183
|
-
writableProjectIds: effective.writeProjectIds,
|
|
187
|
+
writableProjectIds: (session?.effective ?? effective).writeProjectIds,
|
|
184
188
|
allowedActions: role.name === "operator"
|
|
185
189
|
? ["route-request", "inspect-catalog", "answer-user-boundary"]
|
|
186
190
|
: ["perform-global-role-request"]
|
|
@@ -196,17 +200,14 @@ function roleContext(args, store, options) {
|
|
|
196
200
|
diagnostics: `role message list ${name} --diagnostics`,
|
|
197
201
|
retry: `session retry ${name} show`
|
|
198
202
|
},
|
|
199
|
-
nativeTurn:
|
|
200
|
-
|
|
201
|
-
nativeTurnId: sessions.providerBinding.run.nativeTurnId,
|
|
202
|
-
status: sessions.providerBinding.run.status
|
|
203
|
-
},
|
|
203
|
+
nativeTurn: health.nativeTurn,
|
|
204
|
+
health,
|
|
204
205
|
...(role.jobOwner === undefined ? {} : { job: {
|
|
205
206
|
owner: role.jobOwner,
|
|
206
207
|
read: `yui job show ${role.jobOwner.scope.kind === "global" ? "--global" : `--task ${role.jobOwner.scope.taskId}`} --job ${role.jobOwner.jobId} --json`,
|
|
207
208
|
boundary: "Job execution only. This Role is not Operator and owns no Task delivery authority. Read the exact Run from its original message."
|
|
208
209
|
} }),
|
|
209
|
-
retry:
|
|
210
|
+
retry: health.retry,
|
|
210
211
|
// Historical interrupt receipts are available through Session inspection;
|
|
211
212
|
// only this Turn's exact identity is required at entry.
|
|
212
213
|
sessionManifestPath: environment.YUI_SESSION_MANIFEST,
|
|
@@ -510,7 +511,7 @@ function roleSession(args, store, options) {
|
|
|
510
511
|
throw usageError("Usage: yui session retry <role> [show|cancel|disable|enable]");
|
|
511
512
|
}
|
|
512
513
|
return store.transaction(tx => {
|
|
513
|
-
requireRole(name, tx);
|
|
514
|
+
const role = requireRole(name, tx);
|
|
514
515
|
const sessions = tx.getGlobalRoleSessionSet(name);
|
|
515
516
|
let binding = sessions?.providerBinding ?? null;
|
|
516
517
|
if (action !== "show") {
|
|
@@ -520,9 +521,14 @@ function roleSession(args, store, options) {
|
|
|
520
521
|
tx.saveGlobalRoleSessionSet({ ...sessions, providerBinding: binding });
|
|
521
522
|
settleGlobalRetryInput(tx, name, binding, new Date());
|
|
522
523
|
}
|
|
524
|
+
const health = globalSessionHealth(role, sessions);
|
|
525
|
+
const historical = binding?.retry !== undefined && health.retry === null;
|
|
523
526
|
return options.jsonOutput ? JSON.stringify({
|
|
524
|
-
roleName: name, retry: providerRetryProjection(binding), disabled: binding?.retryDisabled ?? false
|
|
525
|
-
|
|
527
|
+
roleName: name, retry: providerRetryProjection(binding), disabled: binding?.retryDisabled ?? false,
|
|
528
|
+
selectedNativeSessionId: health.nativeSessionId,
|
|
529
|
+
retryScope: historical ? "historical-session" : "current-session"
|
|
530
|
+
}) + "\n" : (historical ? "Historical Session evidence (not the selected Session's current failure): " : "")
|
|
531
|
+
+ renderProviderRetry(binding) + "\n";
|
|
526
532
|
});
|
|
527
533
|
}
|
|
528
534
|
if (command !== "record" && command !== "replace") {
|
|
@@ -322,6 +322,7 @@ export function runInboxCommand(args, store, env, now = new Date()) {
|
|
|
322
322
|
if (message.inbox?.ack || message.delivery !== undefined || message.interruptThen !== undefined
|
|
323
323
|
|| message.control !== undefined && message.control.outcome !== "rejected"
|
|
324
324
|
|| turn?.attemptId === `global-input:${current.roleName}/${message.id}`
|
|
325
|
+
&& turn.status !== "rejected"
|
|
325
326
|
|| !foreignTarget && message.notDelivered === undefined) {
|
|
326
327
|
throw new Error("Retirement requires obsolete, proven-unsubmitted input; unknown/accepted/interrupt-owned input must retain its exact handling path.");
|
|
327
328
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { agentAdapterLabel as adapterLabel } from "../agent/adapterCatalog.js";
|
|
2
2
|
import { assertRoleEnabled } from "../role/availability.js";
|
|
3
|
+
import { globalSessionHealth } from "../runtime/globalSessionHealth.js";
|
|
3
4
|
import { usageError } from "../errors/cliError.js";
|
|
4
5
|
import { createRoleSessionSet } from "../executor/agentExecutor.js";
|
|
5
6
|
import { TASK_SUBMISSION_INTENTS } from "../message/message.js";
|
|
@@ -32,7 +33,11 @@ function status(args, store, options) {
|
|
|
32
33
|
if (activeBinding === undefined) {
|
|
33
34
|
throw usageError(`Operator active Agent is not bound: ${role.activeAgentId}.`);
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
+
const sessions = store.getGlobalRoleSessionSet(role.name);
|
|
37
|
+
const projection = {
|
|
38
|
+
...projectOperatorStatus(sessions, role.activeAgentId, activeBinding.adapterId),
|
|
39
|
+
health: globalSessionHealth(role, sessions)
|
|
40
|
+
};
|
|
36
41
|
const writer = projection.writer;
|
|
37
42
|
const writerLines = [
|
|
38
43
|
"Operator status",
|
|
@@ -42,7 +47,10 @@ function status(args, store, options) {
|
|
|
42
47
|
` Adapter: ${adapterLabel(writer.adapterId)}`,
|
|
43
48
|
` Conversation: ${writer.sessionRef ?? "-"}`,
|
|
44
49
|
` Native session: ${writer.nativeSessionId ?? "-"}`,
|
|
45
|
-
` Session state: ${writer.sessionStatus ?? "-"}
|
|
50
|
+
` Session state: ${writer.sessionStatus ?? "-"}`,
|
|
51
|
+
` Configuration: desired r${projection.health.desiredRevision}, effective r${projection.health.effectiveRevision ?? "unknown"}`,
|
|
52
|
+
` Host observation: ${projection.health.hostObservation}`,
|
|
53
|
+
` ${projection.health.note}`
|
|
46
54
|
];
|
|
47
55
|
const history = projection.historicalConversations;
|
|
48
56
|
const historyOutput = history.length === 0
|
package/dist/controlPlaneCli.js
CHANGED
|
@@ -532,8 +532,9 @@ export async function main() {
|
|
|
532
532
|
const result = await callController(home, "controller.status", {}, { timeoutMs: 2_000 });
|
|
533
533
|
runtime = parseControllerRuntimeSnapshot(result, droppedEvents);
|
|
534
534
|
}
|
|
535
|
-
catch {
|
|
536
|
-
runtime = { source: "unsupported", droppedEvents
|
|
535
|
+
catch (error) {
|
|
536
|
+
runtime = { source: "unsupported", droppedEvents,
|
|
537
|
+
error: describeCliFailure(error, failureContext()).message.slice(0, 1000) };
|
|
537
538
|
}
|
|
538
539
|
const mismatch = summarizeDurablePhysicalMismatch(snapshot);
|
|
539
540
|
const identitySection = renderRuntimeIdentitySection({
|
|
@@ -2611,6 +2612,7 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
|
|
|
2611
2612
|
}
|
|
2612
2613
|
await prepareInteractiveCodexApp(control.targetAgentId, store, home);
|
|
2613
2614
|
await ensureFileTaskController(home, { environment: process.env });
|
|
2615
|
+
let stopDisposition = "not-required";
|
|
2614
2616
|
if (paneRunning
|
|
2615
2617
|
|| (active !== undefined
|
|
2616
2618
|
&& active.status === "active")
|
|
@@ -2618,9 +2620,24 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
|
|
|
2618
2620
|
&& ["submitting", "accepted", "delivery-unknown"].includes(sessionSet.providerBinding.run.status))
|
|
2619
2621
|
|| (sessionSet?.providerBinding?.goal != null && sessionSet.providerBinding.goal.status !== "complete")) {
|
|
2620
2622
|
await runtime.stopGlobalRoleSession(role.name);
|
|
2623
|
+
stopDisposition = "completed";
|
|
2624
|
+
}
|
|
2625
|
+
try {
|
|
2626
|
+
applyOperatorSessionControl(control, store);
|
|
2627
|
+
}
|
|
2628
|
+
catch (error) {
|
|
2629
|
+
throw new Error(`Operator replacement selection failed; prior stop=${stopDisposition}; `
|
|
2630
|
+
+ "new Session startup has not begun. Inspect the retained Session state.", { cause: error });
|
|
2631
|
+
}
|
|
2632
|
+
try {
|
|
2633
|
+
await runtime.prepareGlobalRoleEnter(role.name);
|
|
2634
|
+
}
|
|
2635
|
+
catch (error) {
|
|
2636
|
+
throw new Error(`Operator replacement selection committed; prior stop=${stopDisposition}; its startup phase failed `
|
|
2637
|
+
+ "without readiness confirmation. Whether native startup began is unconfirmed. "
|
|
2638
|
+
+ "Inspect the exact current Session/Host before retrying; "
|
|
2639
|
+
+ "do not infer that startup had no effect.", { cause: error });
|
|
2621
2640
|
}
|
|
2622
|
-
applyOperatorSessionControl(control, store);
|
|
2623
|
-
await runtime.prepareGlobalRoleEnter(role.name);
|
|
2624
2641
|
tmux.attachRole("operator", role.name, "auto");
|
|
2625
2642
|
}
|
|
2626
2643
|
async function prepareInteractiveCodexApp(agentId, store, home) {
|
|
@@ -456,10 +456,7 @@ export class FileTaskWorkflowRuntime {
|
|
|
456
456
|
}
|
|
457
457
|
if (targets.length === 0)
|
|
458
458
|
return;
|
|
459
|
-
await
|
|
460
|
-
...this.clientOptions,
|
|
461
|
-
requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
|
|
462
|
-
});
|
|
459
|
+
await Promise.all(targets.map(target => this.#waitForRuntimeCleanup(target)));
|
|
463
460
|
for (const target of targets) {
|
|
464
461
|
if (hasRuntimeLifecycleWork(this.store.getWorkMailbox(target))) {
|
|
465
462
|
throw new Error(`Role runtime did not stop: ${target.taskId}/${target.roleName}.`);
|
|
@@ -487,10 +484,7 @@ export class FileTaskWorkflowRuntime {
|
|
|
487
484
|
if (target === null) {
|
|
488
485
|
throw new Error(`Role Session changed before its exact stop was reserved: ${input.taskId}/${input.roleName}.`);
|
|
489
486
|
}
|
|
490
|
-
await
|
|
491
|
-
...this.clientOptions,
|
|
492
|
-
requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
|
|
493
|
-
});
|
|
487
|
+
await this.#waitForRuntimeCleanup(target);
|
|
494
488
|
if (hasRuntimeLifecycleWork(this.store.getWorkMailbox(target))) {
|
|
495
489
|
throw new Error(`Role runtime did not stop: ${input.taskId}/${input.roleName}.`);
|
|
496
490
|
}
|
|
@@ -506,7 +500,7 @@ export class FileTaskWorkflowRuntime {
|
|
|
506
500
|
.filter((job) => job.taskId === taskId);
|
|
507
501
|
if (active.length === 0)
|
|
508
502
|
return;
|
|
509
|
-
await callFileTaskController(this.home, "scheduler.
|
|
503
|
+
await callFileTaskController(this.home, "scheduler.signal", { key: `task:${encodeURIComponent(taskId)}` }, {
|
|
510
504
|
...this.clientOptions,
|
|
511
505
|
requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
|
|
512
506
|
});
|
|
@@ -585,10 +579,7 @@ export class FileTaskWorkflowRuntime {
|
|
|
585
579
|
if (target?.kind !== "global-role-runtime") {
|
|
586
580
|
throw new Error(`Global Role runtime cleanup target is invalid: ${roleName}.`);
|
|
587
581
|
}
|
|
588
|
-
await
|
|
589
|
-
...this.clientOptions,
|
|
590
|
-
requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
|
|
591
|
-
});
|
|
582
|
+
await this.#waitForRuntimeCleanup(target);
|
|
592
583
|
if (hasRuntimeLifecycleWork(this.store.getWorkMailbox(target))) {
|
|
593
584
|
throw new Error(`Global Role runtime did not stop: ${roleName}.`);
|
|
594
585
|
}
|
|
@@ -598,6 +589,36 @@ export class FileTaskWorkflowRuntime {
|
|
|
598
589
|
throw new Error(`Global Role runtime session is still active: ${roleName}.`);
|
|
599
590
|
}
|
|
600
591
|
}
|
|
592
|
+
/**
|
|
593
|
+
* The durable cleanup mailbox is completed only after stopOwner proves
|
|
594
|
+
* quiescence and commits the existing cleanup transaction. A scheduler hint
|
|
595
|
+
* is not that proof; nor is exhaustion of the Home-wide coalescing loop.
|
|
596
|
+
*/
|
|
597
|
+
async #waitForRuntimeCleanup(target) {
|
|
598
|
+
const timeoutMs = this.clientOptions.requestTimeoutMs ?? LIFECYCLE_REQUEST_TIMEOUT_MS;
|
|
599
|
+
const deadline = Date.now() + timeoutMs;
|
|
600
|
+
const label = controllerMailboxKey(target);
|
|
601
|
+
try {
|
|
602
|
+
await callFileTaskController(this.home, "scheduler.signal", { key: label }, {
|
|
603
|
+
...this.clientOptions, requestTimeoutMs: timeoutMs
|
|
604
|
+
});
|
|
605
|
+
while (hasRuntimeLifecycleWork(this.store.getWorkMailbox(target))) {
|
|
606
|
+
if (Date.now() >= deadline) {
|
|
607
|
+
throw new Error("Timed out awaiting the target's durable cleanup transaction.");
|
|
608
|
+
}
|
|
609
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(this.clientOptions.pollIntervalMs ?? POLL_INTERVAL_MS, Math.max(1, deadline - Date.now()))));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
// A lost hint response must not hide cleanup that actually committed.
|
|
614
|
+
// No hint or native input is replayed here.
|
|
615
|
+
if (!hasRuntimeLifecycleWork(this.store.getWorkMailbox(target)))
|
|
616
|
+
return;
|
|
617
|
+
throw new Error(`Runtime stop remains unconfirmed for ${label}; cleanup obligation retained. `
|
|
618
|
+
+ "No replacement was started by this stop operation. Inspect the exact Session/Host "
|
|
619
|
+
+ "and cleanup failure before retrying; no physical exit is inferred.", { cause: error });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
601
622
|
/** Drain pending runtime facts while an external maintenance fence is held. */
|
|
602
623
|
async drainController() {
|
|
603
624
|
await ensureFileTaskController(this.home, {
|
|
@@ -7,7 +7,7 @@ import { reconcileExitedRoleRuns } from "../scheduler/roleRunLiveness.js";
|
|
|
7
7
|
import { DEFAULT_STALL_WINDOW_MS, DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS, reconcileStalledRoleRuns } from "../scheduler/roleRunStall.js";
|
|
8
8
|
import { processOperatorInputNotifications } from "../scheduler/operatorInputNotificationProcessor.js";
|
|
9
9
|
import { startControllerServer } from "../core/controllerServer.js";
|
|
10
|
-
import { monotonicMilliseconds } from "../core/controllerTelemetry.js";
|
|
10
|
+
import { ControllerWorkObserver, monotonicMilliseconds } from "../core/controllerTelemetry.js";
|
|
11
11
|
import { isProjectMaintenanceFenced } from "../repository/projectMaintenanceLock.js";
|
|
12
12
|
import { isHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
13
13
|
import { KeyedWorkQueue } from "../coordination/keyedWorkQueue.js";
|
|
@@ -44,7 +44,9 @@ const ZERO_DRAIN_METRICS = Object.freeze({
|
|
|
44
44
|
* Runs one lean scheduler pass. Due native Turn completions are folded before
|
|
45
45
|
* liveness, so a valid Hook boundary fences destructive process reconciliation.
|
|
46
46
|
*/
|
|
47
|
-
export async function runControllerSchedulerPass(store, delivery, now, workspacePreparer, scope = { kind: "full" }, includeOperator = true, runtimeCleanupOutcomes = [], lifecycleHost, stallWindowMs = DEFAULT_STALL_WINDOW_MS, maintenanceFence, onMaintenanceFenceDefer, blockedTaskIds = new Set(), diagnosticAfterMs = DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS, leaderWakeFence, onError) {
|
|
47
|
+
export async function runControllerSchedulerPass(store, delivery, now, workspacePreparer, scope = { kind: "full" }, includeOperator = true, runtimeCleanupOutcomes = [], lifecycleHost, stallWindowMs = DEFAULT_STALL_WINDOW_MS, maintenanceFence, onMaintenanceFenceDefer, blockedTaskIds = new Set(), diagnosticAfterMs = DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS, leaderWakeFence, onError, observer) {
|
|
48
|
+
const target = scope.kind === "full" ? "home" : scope.keys.slice(0, 16).join(",");
|
|
49
|
+
const observe = (phase, action) => observer === undefined ? Promise.resolve().then(action) : observer.observe(phase, target, action);
|
|
48
50
|
const compiledSelection = compileReconcileSelection(scope);
|
|
49
51
|
const selection = includeOperator
|
|
50
52
|
? { ...compiledSelection, blockedTaskIds }
|
|
@@ -54,7 +56,7 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
|
|
|
54
56
|
// event-loop turn. Give already-written requests a poll boundary before the
|
|
55
57
|
// next durable phase; later phases retain their existing CAS fences.
|
|
56
58
|
await controlEventLoopRun();
|
|
57
|
-
const failedCleanupRoles = await processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost, scope, now, runtimeCleanupOutcomes, blockedTaskIds);
|
|
59
|
+
const failedCleanupRoles = await observe("runtime-cleanup", () => processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost, scope, now, runtimeCleanupOutcomes, blockedTaskIds));
|
|
58
60
|
const roleSelection = selectionWithoutFailedCleanupRoles(store, selection, failedCleanupRoles);
|
|
59
61
|
const availableWakeupSelection = () => (leaderWakeFence?.() === true
|
|
60
62
|
? exactTaskSelection(new Set())
|
|
@@ -64,19 +66,19 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
|
|
|
64
66
|
// Durable Leader work that already has a ready Task workspace belongs to
|
|
65
67
|
// the control path. Dispatch it before any unrelated Task workspace I/O;
|
|
66
68
|
// the processor itself retains the fail-closed workspace-ready guard.
|
|
67
|
-
const initialWakeups = selectedPendingWakeups(store, availableWakeupSelection());
|
|
68
|
-
const initialWakeupResults = await processLeaderWakeups(store, delivery, now, exactTaskSelection(new Set(initialWakeups.keys())));
|
|
69
|
+
const initialWakeups = await observe("wakeup-projection", () => selectedPendingWakeups(store, availableWakeupSelection()));
|
|
70
|
+
const initialWakeupResults = await observe("leader-delivery", () => processLeaderWakeups(store, delivery, now, exactTaskSelection(new Set(initialWakeups.keys()))));
|
|
69
71
|
// Preserve ready-Leader-first ordering, then bound the repeated state
|
|
70
72
|
// projections that follow it in this pass.
|
|
71
73
|
await controlEventLoopRun();
|
|
72
74
|
// A Task whose deferred activation was just released must become active
|
|
73
75
|
// before the workspace phase, so its first workspace preparation happens in
|
|
74
76
|
// this same pass rather than waiting for the next one.
|
|
75
|
-
await adoptReleasedTaskActivations(store, workspacePreparer, selection, onError);
|
|
77
|
+
await observe("activation", () => adoptReleasedTaskActivations(store, workspacePreparer, selection, onError));
|
|
76
78
|
await controlEventLoopRun();
|
|
77
|
-
const workspacePreparation = await prepareActiveWorkspaces(store, workspacePreparer, selection, maintenanceFence, onMaintenanceFenceDefer);
|
|
79
|
+
const workspacePreparation = await observe("workspace-preparation", () => prepareActiveWorkspaces(store, workspacePreparer, selection, maintenanceFence, onMaintenanceFenceDefer));
|
|
78
80
|
await controlEventLoopRun();
|
|
79
|
-
const activeRunDeliveries = await processActiveRoleRunDeliveries(store, delivery, now, roleSelection);
|
|
81
|
+
const activeRunDeliveries = await observe("run-delivery", () => processActiveRoleRunDeliveries(store, delivery, now, roleSelection));
|
|
80
82
|
await controlEventLoopRun();
|
|
81
83
|
const unsettledRunRefs = new Set(activeRunDeliveries.flatMap((result) => (result.reason === "delivery-uncertain"
|
|
82
84
|
? [formatTaskRecordReference(result.taskId, result.runId, "run")]
|
|
@@ -108,16 +110,16 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
|
|
|
108
110
|
await controlEventLoopRun();
|
|
109
111
|
const liveStatuses = new Map();
|
|
110
112
|
const resourceEvidence = new Map();
|
|
111
|
-
const failedRunRefs = await reconcileExitedRoleRuns(store, delivery, now, roleSelection, unsettledRunRefs, liveStatuses, resourceEvidence, scope.kind === "dirty");
|
|
113
|
+
const failedRunRefs = await observe("run-liveness", () => reconcileExitedRoleRuns(store, delivery, now, roleSelection, unsettledRunRefs, liveStatuses, resourceEvidence, scope.kind === "dirty"));
|
|
112
114
|
await controlEventLoopRun();
|
|
113
115
|
await reconcileStalledRoleRuns(store, delivery, now, roleSelection, stallWindowMs, liveStatuses, resourceEvidence, diagnosticAfterMs);
|
|
114
116
|
await controlEventLoopRun();
|
|
115
117
|
const selectedInputTaskIds = selectedTaskIdsForBoundedPass(store, selection);
|
|
116
118
|
const autoResolvedInputs = selectedInputTaskIds === undefined
|
|
117
|
-
? store.resolveExpiredInputRecommendations(now)
|
|
119
|
+
? await store.resolveExpiredInputRecommendations(now)
|
|
118
120
|
: selectedInputTaskIds.size === 0
|
|
119
121
|
? []
|
|
120
|
-
: store.resolveExpiredInputRecommendations(now, selectedInputTaskIds);
|
|
122
|
+
: await store.resolveExpiredInputRecommendations(now, selectedInputTaskIds);
|
|
121
123
|
// Liveness and auto-resolution can durably queue new Leader work. Process
|
|
122
124
|
// only Tasks that were not part of phase one, except when a same-pass due
|
|
123
125
|
// completion made an initially busy Leader idle. Result order remains
|
|
@@ -691,10 +693,15 @@ export class FileTaskController {
|
|
|
691
693
|
#jobSupervisor;
|
|
692
694
|
#schedules;
|
|
693
695
|
#globalInputDelivery;
|
|
696
|
+
#messageHealth;
|
|
697
|
+
#lastMessageHealth = null;
|
|
694
698
|
#providerRetry;
|
|
695
699
|
#continuationReconciler;
|
|
696
700
|
#hostTerminalRecovery;
|
|
701
|
+
#persistenceMetrics;
|
|
702
|
+
#diagnosticMetrics;
|
|
697
703
|
#current;
|
|
704
|
+
#phases = new ControllerWorkObserver();
|
|
698
705
|
#operatorCurrent;
|
|
699
706
|
#runtimeObserverCurrent;
|
|
700
707
|
#pendingFull = false;
|
|
@@ -712,6 +719,8 @@ export class FileTaskController {
|
|
|
712
719
|
constructor(store, delivery, options = {}) {
|
|
713
720
|
this.store = store;
|
|
714
721
|
this.delivery = delivery;
|
|
722
|
+
this.#persistenceMetrics = options.persistenceMetrics;
|
|
723
|
+
this.#diagnosticMetrics = options.diagnosticMetrics;
|
|
715
724
|
this.#intervalMs = positiveInteger(options.intervalMs, DEFAULT_RECONCILIATION_INTERVAL_MS, "Controller reconciliation interval");
|
|
716
725
|
this.#now = options.now ?? (() => new Date());
|
|
717
726
|
this.#onError = options.onError ?? (() => { });
|
|
@@ -746,6 +755,7 @@ export class FileTaskController {
|
|
|
746
755
|
this.#continuationReconciler = options.continuationReconciler;
|
|
747
756
|
this.#hostTerminalRecovery = options.hostTerminalRecovery;
|
|
748
757
|
this.#globalInputDelivery = options.globalInputDelivery;
|
|
758
|
+
this.#messageHealth = options.messageHealth;
|
|
749
759
|
this.#providerRetry = options.providerRetry;
|
|
750
760
|
this.#signalScheduler = new MailboxScheduler(async (keys) => { await this.#requestPass({ kind: "dirty", keys }); }, {
|
|
751
761
|
windowMs: options.signalWindowMs ?? DEFAULT_SIGNAL_WINDOW_MS,
|
|
@@ -782,6 +792,13 @@ export class FileTaskController {
|
|
|
782
792
|
runtimeMetrics() {
|
|
783
793
|
const metrics = this.#lastRuntimeDrain?.metrics ?? ZERO_DRAIN_METRICS;
|
|
784
794
|
return {
|
|
795
|
+
messageHealth: this.#lastMessageHealth,
|
|
796
|
+
diagnostics: this.#diagnosticMetrics?.() ?? null,
|
|
797
|
+
phases: this.#phases.snapshot(),
|
|
798
|
+
persistence: this.#persistenceMetrics?.() ?? null,
|
|
799
|
+
reads: this.store.readObservationMetrics?.() ?? null,
|
|
800
|
+
scheduler: { running: this.#current !== undefined, pendingFull: this.#pendingFull,
|
|
801
|
+
pendingKeys: this.#pendingKeys.size },
|
|
785
802
|
// Status is intentionally O(1): scanning the inbox while serving the
|
|
786
803
|
// control socket would recreate the starvation this metric diagnoses.
|
|
787
804
|
inbox: {
|
|
@@ -956,7 +973,7 @@ export class FileTaskController {
|
|
|
956
973
|
* without bypassing the scheduler's own ownership and delivery fences. */
|
|
957
974
|
async #observeMaintenance(phase, action) {
|
|
958
975
|
try {
|
|
959
|
-
return await action
|
|
976
|
+
return await this.#phases.observe(phase, "home", action);
|
|
960
977
|
}
|
|
961
978
|
catch (error) {
|
|
962
979
|
this.#onError(new Error(`${phase} failed: ${error instanceof Error ? error.message : String(error)}. `
|
|
@@ -998,12 +1015,18 @@ export class FileTaskController {
|
|
|
998
1015
|
}
|
|
999
1016
|
}
|
|
1000
1017
|
}
|
|
1001
|
-
const firstRuntimeDrain = await this.#drainRuntimeEvents();
|
|
1018
|
+
const firstRuntimeDrain = await this.#phases.observe("runtime-drain", "home", () => this.#drainRuntimeEvents());
|
|
1002
1019
|
if (scope.kind === "full" && this.#hostTerminalRecovery !== undefined) {
|
|
1003
1020
|
await this.#observeMaintenance("Host terminal recovery", this.#hostTerminalRecovery);
|
|
1004
1021
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
+
}
|
|
1028
|
+
await this.#phases.observe("provider-retry", "home", () => this.#providerRetry?.reconcile());
|
|
1029
|
+
await this.#phases.observe("global-input", "home", () => this.#globalInputDelivery?.({ reconcile: scope.kind === "full", now: this.#now() }));
|
|
1007
1030
|
for (const taskId of runtimeTaskFailureIds(firstRuntimeDrain)) {
|
|
1008
1031
|
runtimeFailedTaskIds.add(taskId);
|
|
1009
1032
|
}
|
|
@@ -1016,17 +1039,17 @@ export class FileTaskController {
|
|
|
1016
1039
|
// DurableJob reconciliation runs before the scheduler pass so a
|
|
1017
1040
|
// terminal job's Leader wakeup is enqueued in the same pass that
|
|
1018
1041
|
// processes Leader wakeups.
|
|
1019
|
-
this.#schedules?.reconcile(this.#now());
|
|
1042
|
+
await this.#schedules?.reconcile(this.#now());
|
|
1020
1043
|
this.#jobSupervisor?.reconcile(this.#now());
|
|
1021
1044
|
if (scope.kind === "full") {
|
|
1022
|
-
result = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, scope, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, runtimeFailedTaskIds, this.#diagnosticAfterMs, this.#leaderWakeFence, this.#onError);
|
|
1045
|
+
result = await this.#phases.observe("scheduler-full", "home", () => runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, scope, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, runtimeFailedTaskIds, this.#diagnosticAfterMs, this.#leaderWakeFence, this.#onError, this.#phases));
|
|
1023
1046
|
}
|
|
1024
1047
|
else {
|
|
1025
|
-
const dirtyPass = await this.#runDirtySchedulerPass(scope, runtimeCleanupOutcomes, runtimeFailedTaskIds);
|
|
1048
|
+
const dirtyPass = await this.#phases.observe("scheduler-dirty", scope.keys.slice(0, 16).join(","), () => this.#runDirtySchedulerPass(scope, runtimeCleanupOutcomes, runtimeFailedTaskIds));
|
|
1026
1049
|
result = dirtyPass.result;
|
|
1027
1050
|
failedTaskScopes = dirtyPass.failedTaskScopes;
|
|
1028
1051
|
}
|
|
1029
|
-
const secondRuntimeDrain = await this.#drainRuntimeEvents();
|
|
1052
|
+
const secondRuntimeDrain = await this.#phases.observe("runtime-drain", "home", () => this.#drainRuntimeEvents());
|
|
1030
1053
|
for (const taskId of runtimeTaskFailureIds(secondRuntimeDrain)) {
|
|
1031
1054
|
runtimeFailedTaskIds.add(taskId);
|
|
1032
1055
|
}
|
|
@@ -1040,7 +1063,7 @@ export class FileTaskController {
|
|
|
1040
1063
|
// scheduler.
|
|
1041
1064
|
if (scope.kind === "full" && this.#resourceAutoGc !== undefined) {
|
|
1042
1065
|
try {
|
|
1043
|
-
const gc = await this.#resourceAutoGc
|
|
1066
|
+
const gc = await this.#phases.observe("resource-gc", "home", this.#resourceAutoGc);
|
|
1044
1067
|
if (!gc.skipped && gc.failed > 0) {
|
|
1045
1068
|
this.#onError(new Error(`Resource auto-GC failed to quarantine ${gc.failed} `
|
|
1046
1069
|
+ "resource(s); they stay in place and retry next pass."));
|
|
@@ -1120,7 +1143,7 @@ export class FileTaskController {
|
|
|
1120
1143
|
const taskScopes = partition.taskScopes.filter((taskScope) => (!blockedTaskIds.has(taskScope.taskId)));
|
|
1121
1144
|
const orderedResults = [];
|
|
1122
1145
|
if (partition.globalKeys.length > 0) {
|
|
1123
|
-
orderedResults.push(await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: partition.globalKeys }, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#diagnosticAfterMs, this.#leaderWakeFence, this.#onError));
|
|
1146
|
+
orderedResults.push(await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: partition.globalKeys }, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#diagnosticAfterMs, this.#leaderWakeFence, this.#onError, this.#phases));
|
|
1124
1147
|
}
|
|
1125
1148
|
if (taskScopes.length === 0
|
|
1126
1149
|
|| this.#stopped
|
|
@@ -1153,7 +1176,7 @@ export class FileTaskController {
|
|
|
1153
1176
|
if (this.#stopped || this.#pendingFull)
|
|
1154
1177
|
continue;
|
|
1155
1178
|
try {
|
|
1156
|
-
taskResults[selected.index] = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: selected.taskScope.keys }, false, taskCleanupOutcomes[selected.index], this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#diagnosticAfterMs, this.#leaderWakeFence, this.#onError);
|
|
1179
|
+
taskResults[selected.index] = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: selected.taskScope.keys }, false, taskCleanupOutcomes[selected.index], this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#diagnosticAfterMs, this.#leaderWakeFence, this.#onError, this.#phases);
|
|
1157
1180
|
this.#clearTaskPassRetry(selected.taskScope.taskId);
|
|
1158
1181
|
}
|
|
1159
1182
|
catch (error) {
|