@zq-silk/yui 2.16.0 → 2.18.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 +19 -12
- package/dist/cli/interactionCandidates.js +20 -5
- package/dist/cli/interactionPolicy.js +5 -4
- package/dist/cli/roleWizard.js +7 -0
- package/dist/commands/controllerCommands.js +13 -4
- package/dist/commands/durableJobCommands.js +50 -3
- package/dist/commands/globalRoleCommands.js +31 -14
- package/dist/commands/inboxCommands.js +1 -0
- package/dist/commands/operatorCommands.js +12 -2
- package/dist/commands/profileCommands.js +19 -2
- package/dist/commands/roleConfiguration.js +3 -0
- package/dist/commands/taskCommands.js +16 -0
- package/dist/commands/taskRoleRuntimeStatus.js +3 -0
- package/dist/controlPlaneCli.js +21 -4
- package/dist/controller/agentJobControl.js +9 -1
- package/dist/controller/clientRuntime.js +34 -13
- package/dist/controller/controller.js +35 -22
- package/dist/controller/diagnosticJournal.js +214 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +174 -105
- package/dist/controller/globalInputDelivery.js +19 -9
- 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/providerRetryAdmission.js +9 -0
- package/dist/controller/providerRetryDelivery.js +16 -0
- package/dist/controller/runtime.js +80 -4
- package/dist/controller/runtimeLaunchCoordinator.js +3 -1
- 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/executor/fileRoleLaunchPlanner.js +3 -0
- 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/message/messageContinuation.js +4 -0
- package/dist/output/rolePresentation.js +3 -0
- package/dist/profile/agentProfile.js +8 -0
- package/dist/profile/agentProfileRuntime.js +16 -1
- package/dist/review/reviewDecision.js +6 -0
- package/dist/role/availability.js +28 -0
- package/dist/role/role.js +6 -0
- package/dist/runtime/builtinAgentErrorMappers.js +4 -0
- package/dist/runtime/codexNativeAccess.js +21 -4
- package/dist/runtime/globalSessionHealth.js +39 -0
- package/dist/runtime/tmuxAdapters.js +5 -0
- package/dist/scheduler/activeRoleRunDelivery.js +9 -0
- package/dist/scheduler/leaderWakeupProcessor.js +8 -1
- package/dist/scheduler/roleRunLiveness.js +3 -0
- package/dist/scheduler/roleRunStall.js +10 -10
- package/dist/storage/persistenceWorker.js +41 -19
- package/dist/storage/sqliteSchema.js +43 -9
- package/dist/storage/sqliteStore.js +86 -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 +32 -28
- 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/app/taskView.js +7 -11
- package/dist/web/assets/client/i18n/en.js +265 -36
- package/dist/web/assets/client/i18n/zh-CN.js +264 -35
- package/dist/web/assets/client/index.js +4 -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/layout/shortcuts.js +2 -2
- package/dist/web/assets/client/layout/workspace.js +99 -112
- package/dist/web/assets/client/lib/prefs.js +1 -1
- package/dist/web/assets/client/ui/primitives.js +15 -2
- package/dist/web/assets/client/views/dock/conversation.js +421 -123
- package/dist/web/assets/client/views/dock/session.js +229 -0
- package/dist/web/assets/client/views/dock/terminal.js +48 -39
- package/dist/web/assets/client/views/jobs.js +1042 -160
- package/dist/web/assets/client/views/overview.js +2 -2
- package/dist/web/assets/client/views/sidebar.js +3 -0
- package/dist/web/assets/client/views/task/page.js +2 -12
- package/dist/web/assets/shared/manageNav.js +3 -1
- package/dist/web/assets/shell/appRail.js +2 -1
- package/dist/web/assets/shell/dock.js +9 -27
- 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/conversation.js +96 -17
- package/dist/web/assets/styles/views/dock.js +17 -5
- package/dist/web/assets/styles/views/jobs.js +301 -12
- package/dist/web/assets/styles/views/sidebar.js +1 -2
- package/dist/web/assets/styles/views/task.js +1 -2
- package/dist/web/assets/styles/views/terminal.js +12 -8
- package/dist/web/webConversation.js +1 -0
- package/dist/web/webServer.js +28 -1
- package/dist/web/webSettings.js +10 -2
- package/docs/global-shell-jobs.md +78 -0
- package/docs/job-runs.md +6 -3
- package/docs/managed-turn-and-session-runtime.md +84 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +57 -0
- package/docs/release-workflow.md +67 -0
- package/docs/release-workflow.zh-CN.md +48 -0
- package/docs/roles-and-configuration.md +45 -1
- package/docs/roles-and-configuration.zh-CN.md +35 -0
- package/docs/storage-baseline.md +19 -1
- package/docs/storage-baseline.zh-CN.md +10 -1
- package/docs/testing/review-evaluation.md +35 -1
- package/docs/testing/review-evaluation.zh-CN.md +25 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/execution.md +19 -8
- package/skills/yui-reviewer/SKILL.md +13 -2
- package/skills/yui-reviewer/VERSION +1 -1
|
@@ -102,15 +102,19 @@ export function runtimeDiagnosticEvidence(messages, key) {
|
|
|
102
102
|
/** Global observations have no Task event history. Keep original failed
|
|
103
103
|
* attempts as ordinary record-only Messages before replacing the live binding. */
|
|
104
104
|
export function recordGlobalRuntimeFailure(store, roleName, identity, facts, now) {
|
|
105
|
+
return recordFailure(store, roleName, identity, facts, now, [...store.listGlobalRoleMessages(roleName)]);
|
|
106
|
+
}
|
|
107
|
+
function recordFailure(store, roleName, identity, facts, now, messages) {
|
|
105
108
|
const diagnostic = diagnosticIdentity(store, roleName, identity, facts);
|
|
106
109
|
const header = `${FAILURE_HEADER}${hash([identity, diagnostic.key])}\n`;
|
|
107
|
-
const previous =
|
|
110
|
+
const previous = messages.find(message => message.kind === "system" && message.body.startsWith(header));
|
|
108
111
|
if (previous !== undefined)
|
|
109
112
|
return `${roleName}/${previous.id}`;
|
|
110
113
|
const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), roleName, header + redactAgentErrorText(JSON.stringify({
|
|
111
114
|
...object(facts), diagnostic
|
|
112
115
|
})), "system", { type: "system" }, now);
|
|
113
116
|
store.saveGlobalRoleMessage(message);
|
|
117
|
+
messages.push(message);
|
|
114
118
|
return `${roleName}/${message.id}`;
|
|
115
119
|
}
|
|
116
120
|
/** Reuse the Global Message inbox for an exact failure, not a recovery queue.
|
|
@@ -119,9 +123,13 @@ export function recordGlobalRuntimeAttention(store, roleName, identity, facts, n
|
|
|
119
123
|
// A Home may have a custom Global Role without an Operator. Keep its own
|
|
120
124
|
// diagnostic readable; never create a Role or launch a substitute Agent.
|
|
121
125
|
const recipient = store.getGlobalRole("operator") === null ? roleName : "operator";
|
|
122
|
-
|
|
126
|
+
// These observations run in the caller's transaction. Reuse that exact
|
|
127
|
+
// history plus our appended record, rather than loading/parsing the whole
|
|
128
|
+
// Role history twice on every unchanged Host failure.
|
|
129
|
+
const messages = [...store.listGlobalRoleMessages(roleName)];
|
|
130
|
+
const failureRef = recordFailure(store, roleName, identity, facts, now, messages);
|
|
123
131
|
const diagnostic = diagnosticIdentity(store, roleName, identity, facts);
|
|
124
|
-
const group = runtimeDiagnosticGroups(
|
|
132
|
+
const group = runtimeDiagnosticGroups(messages)
|
|
125
133
|
.reverse().find(group => group.key === diagnostic.key && group.recoveredAt === undefined);
|
|
126
134
|
const requestId = `runtime-attention:${hash([roleName, diagnostic.key, group?.episode ?? identity])}`;
|
|
127
135
|
if (store.queryGlobalRoleMessages(recipient, { requestId, limit: 1 }).messages.length > 0)
|
|
@@ -277,7 +277,7 @@ export function assertNonSecretJobInput(params) {
|
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
const input = JSON.stringify({
|
|
280
|
-
env: params.env, steps: params.steps, requestId: params.requestId
|
|
280
|
+
env: params.env, steps: params.steps, requestId: params.requestId, files: params.files
|
|
281
281
|
});
|
|
282
282
|
// Reject recognizable key material regardless of the parameter name, and
|
|
283
283
|
// URL userinfo before either hashing or persisting the specification.
|
|
@@ -5,6 +5,7 @@ import { requireKnownFields } from "../domain/validation.js";
|
|
|
5
5
|
import { createJobDefinition, reviseJobDefinition, jobScopeKey, validateJobDefinition } from "../job/jobDefinition.js";
|
|
6
6
|
import { acknowledgeUnknownDurableJob, requestDurableJobCancel, validateDurableJob, JOB_RUNNER_IMPLEMENTATION, executionNamespace } from "../job/durableJob.js";
|
|
7
7
|
import { prepareBuiltinJob, projectRefreshDigest } from "../job/builtinJob.js";
|
|
8
|
+
import { prepareGlobalShell } from "../job/globalShell.js";
|
|
8
9
|
import { projectRefreshTargetDigest, requireRefreshProject } from "../repository/projectRefresh.js";
|
|
9
10
|
import { assertCallerAuthorized, assertNonSecretJobInput, createDurableJobControl, jobAuthorityBinding, validateStartParams, requireJobAssignment, parseDurableJobCaller } from "./jobControl.js";
|
|
10
11
|
import { createFileArtifactPort } from "./jobSupervisor.js";
|
|
@@ -38,7 +39,7 @@ export function authorizeScope(store, scope, caller, allowPublic = false) {
|
|
|
38
39
|
assertCallerAuthorized(store, caller, scope.kind === "task" ? scope.taskId : "");
|
|
39
40
|
}
|
|
40
41
|
function taskParams(job, caller, requestId) {
|
|
41
|
-
if (job.scope.kind !== "task" || job.configuration.kind !== "shell")
|
|
42
|
+
if (job.scope.kind !== "task" || job.configuration.kind !== "shell" || !("owner" in job.configuration))
|
|
42
43
|
throw new Error("Not a Task Job.");
|
|
43
44
|
return { ...job.configuration, taskId: job.scope.taskId, caller, requestId };
|
|
44
45
|
}
|
|
@@ -60,8 +61,13 @@ export function authorizeDefinition(store, job, caller, write) {
|
|
|
60
61
|
assertNonSecretJobInput(params);
|
|
61
62
|
}
|
|
62
63
|
else if (write) {
|
|
64
|
+
if (job.configuration.kind === "shell") {
|
|
65
|
+
prepareGlobalShell(job.configuration, "/job-validation");
|
|
66
|
+
assertNonSecretJobInput({ ...job.configuration, requestId: "global-shell-definition" });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
63
69
|
if (job.configuration.kind !== "builtin")
|
|
64
|
-
throw new Error("
|
|
70
|
+
throw new Error("Not a global process Job.");
|
|
65
71
|
prepareBuiltinJob(job.configuration, "/");
|
|
66
72
|
if (job.configuration.action === "project.refresh") {
|
|
67
73
|
if (job.builtin)
|
|
@@ -190,18 +196,21 @@ export function admitJobRun(store, home, scope, definitionId, caller, trigger, n
|
|
|
190
196
|
}, now);
|
|
191
197
|
return { run: result.job, created: result.created };
|
|
192
198
|
}
|
|
193
|
-
if (job.configuration.kind !== "builtin")
|
|
194
|
-
throw new Error("
|
|
199
|
+
if (job.configuration.kind !== "builtin" && job.configuration.kind !== "shell")
|
|
200
|
+
throw new Error("Not a global process Job.");
|
|
195
201
|
const id = `job-${randomUUID()}`;
|
|
196
202
|
const locator = `artifacts/jobs/global/${id}`;
|
|
197
|
-
const
|
|
203
|
+
const config = job.configuration;
|
|
204
|
+
const shell = config.kind === "shell";
|
|
205
|
+
const spec = shell ? prepareGlobalShell(config, join(home, locator, "workspace"))
|
|
206
|
+
: prepareBuiltinJob(config, join(home, locator), { home, runId: id });
|
|
198
207
|
const actorId = service ? "service:schedule" : caller.scope === "user" ? "user" : `global:${caller.role}`;
|
|
199
208
|
let run = {
|
|
200
209
|
schemaVersion: 1, scope: "global", id, definitionId: job.id, definitionRevision: job.revision,
|
|
201
|
-
trigger: structuredClone(trigger), action:
|
|
210
|
+
trigger: structuredClone(trigger), action: shell ? "shell.exec" : config.action,
|
|
202
211
|
...(service === undefined ? {} : { serviceAuthorization: structuredClone(service) }),
|
|
203
|
-
actionInput: structuredClone(
|
|
204
|
-
...(
|
|
212
|
+
actionInput: structuredClone(shell ? config : config.input),
|
|
213
|
+
...(!shell && config.projectId !== undefined ? { projectId: config.projectId } : {}),
|
|
205
214
|
...spec, status: "queued", artifactsLocator: locator,
|
|
206
215
|
idempotencyKey: createHash("sha256").update(JSON.stringify([actorId, requestId])).digest("hex"),
|
|
207
216
|
operation: {
|
|
@@ -280,10 +289,12 @@ export function authorizeGlobalJobStart(store, run) {
|
|
|
280
289
|
const projects = run.action === "project.refresh" ? store.listProjects() : [];
|
|
281
290
|
const home = store.rootDirectory();
|
|
282
291
|
try {
|
|
283
|
-
const compiled =
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
292
|
+
const compiled = run.action === "shell.exec"
|
|
293
|
+
? prepareGlobalShell(run.actionInput, join(home, "artifacts/jobs/global", run.id, "workspace"))
|
|
294
|
+
: prepareBuiltinJob({
|
|
295
|
+
kind: "builtin", action: run.action, input: run.actionInput,
|
|
296
|
+
...(run.projectId === undefined ? {} : { projectId: run.projectId })
|
|
297
|
+
}, run.workspace, { home, runId: run.id });
|
|
287
298
|
if (run.action === "project.refresh") {
|
|
288
299
|
const definition = store.getJobDefinition({ kind: "global" }, run.definitionId);
|
|
289
300
|
if (definition?.builtin)
|
|
@@ -297,7 +308,7 @@ export function authorizeGlobalJobStart(store, run) {
|
|
|
297
308
|
throw new Error("Project refresh Run workspace does not match its artifact directory.");
|
|
298
309
|
}
|
|
299
310
|
}
|
|
300
|
-
if (JSON.stringify(compiled.steps) !== JSON.stringify(run.steps)
|
|
311
|
+
if (compiled.workspace !== run.workspace || JSON.stringify(compiled.steps) !== JSON.stringify(run.steps)
|
|
301
312
|
|| JSON.stringify(compiled.env) !== JSON.stringify(run.env) || compiled.head !== run.head) {
|
|
302
313
|
throw new Error("Global JobRun does not match its controlled action.");
|
|
303
314
|
}
|
|
@@ -199,6 +199,8 @@ export class DurableJobSupervisor {
|
|
|
199
199
|
return;
|
|
200
200
|
}
|
|
201
201
|
const spec = {
|
|
202
|
+
...(!("taskId" in job) && "action" in job && job.action === "shell.exec"
|
|
203
|
+
? { files: job.actionInput.files } : {}),
|
|
202
204
|
jobId: job.id,
|
|
203
205
|
...("taskId" in job ? { taskId: job.taskId } : { scope: "global" }),
|
|
204
206
|
workspace: job.workspace,
|
|
@@ -386,6 +388,8 @@ export class DurableJobSupervisor {
|
|
|
386
388
|
return fallback;
|
|
387
389
|
if (job.action === "system.noop")
|
|
388
390
|
return { ...fallback, data: { action: "system.noop" } };
|
|
391
|
+
if (job.action === "shell.exec")
|
|
392
|
+
return fallback;
|
|
389
393
|
const raw = this.#artifacts.readBusinessResult?.(executionNamespace(job), job.id);
|
|
390
394
|
if (raw !== null && raw !== undefined) {
|
|
391
395
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertRoleEnabled, isEnabled } from "../role/availability.js";
|
|
1
2
|
import { admitProviderRetry, cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
|
|
2
3
|
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
3
4
|
import { validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
|
|
@@ -20,6 +21,8 @@ export function ownedProviderInput(input) {
|
|
|
20
21
|
export function retryIntentBlocker(store, input, retry) {
|
|
21
22
|
if (input.taskId === undefined) {
|
|
22
23
|
const role = store.getGlobalRole(input.roleName);
|
|
24
|
+
if (role !== null && !isEnabled(role))
|
|
25
|
+
return "role-disabled";
|
|
23
26
|
const sessions = store.getGlobalRoleSessionSet(input.roleName);
|
|
24
27
|
if (role === null || sessions?.activeAgentId !== role.activeAgentId)
|
|
25
28
|
return "role-changed";
|
|
@@ -38,6 +41,8 @@ export function retryIntentBlocker(store, input, retry) {
|
|
|
38
41
|
|| task.executionGate.state !== "enabled")
|
|
39
42
|
return "task-execution-stopped";
|
|
40
43
|
const role = store.getRole(input.taskId, input.roleName);
|
|
44
|
+
if (role !== null && !isEnabled(role))
|
|
45
|
+
return "role-disabled";
|
|
41
46
|
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
42
47
|
if (role === null || sessions?.activeAgentId !== role.activeAgentId)
|
|
43
48
|
return "role-changed";
|
|
@@ -54,6 +59,10 @@ export function retryIntentBlocker(store, input, retry) {
|
|
|
54
59
|
/** Shared by automatic recovery and every ordinary Host submit. The native
|
|
55
60
|
* writer CAS remains the only grant; a scheduled successor is not a grant. */
|
|
56
61
|
export function admitOwnedProviderInput(store, binding, input) {
|
|
62
|
+
const role = input.taskId === undefined ? store.getGlobalRole(input.roleName)
|
|
63
|
+
: store.getRole(input.taskId, input.roleName);
|
|
64
|
+
if (role !== null)
|
|
65
|
+
assertRoleEnabled(role);
|
|
57
66
|
const retry = binding.retry;
|
|
58
67
|
const sameReviewSuccessor = retry?.successorReviewRoundId !== undefined && input.taskId !== undefined && input.runId !== undefined
|
|
59
68
|
&& store.getRun(input.taskId, input.runId)?.reviewRoundId === retry.successorReviewRoundId;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { isEnabled } from "../role/availability.js";
|
|
2
3
|
import { runTaskCommand } from "../commands/taskCommands.js";
|
|
3
4
|
import { routeRoleEvent } from "../scheduler/operatorEvent.js";
|
|
4
5
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
@@ -64,12 +65,19 @@ export function createProviderRetryHooks(home, store, ports = {}) {
|
|
|
64
65
|
const key = (set) => set.owner.scope === "task"
|
|
65
66
|
? `role:${encodeURIComponent(set.owner.taskId)}/${encodeURIComponent(set.owner.roleName)}`
|
|
66
67
|
: `global-role:${encodeURIComponent(set.owner.roleName)}`;
|
|
68
|
+
const disabled = (set) => {
|
|
69
|
+
const role = set.owner.scope === "task"
|
|
70
|
+
? store.getRole(set.owner.taskId, set.owner.roleName) : store.getGlobalRole(set.owner.roleName);
|
|
71
|
+
return role !== null && !isEnabled(role);
|
|
72
|
+
};
|
|
67
73
|
return {
|
|
68
74
|
deadlines() {
|
|
69
75
|
return store.listProviderRetrySessions().flatMap(set => {
|
|
70
76
|
const retry = set.providerBinding.retry;
|
|
71
77
|
if (retry.status !== "waiting")
|
|
72
78
|
return [];
|
|
79
|
+
if (disabled(set))
|
|
80
|
+
return [];
|
|
73
81
|
// Busy or already-queued work awaits the next ordinary pass without a
|
|
74
82
|
// zero-delay timer loop. Waiting never consumes an execution attempt.
|
|
75
83
|
const wall = (ports.now ?? (() => new Date()))().getTime();
|
|
@@ -97,6 +105,10 @@ export function createProviderRetryHooks(home, store, ports = {}) {
|
|
|
97
105
|
const scoped = { ...(owner.scope === "task" ? { taskId: owner.taskId } : {}), roleName: owner.roleName };
|
|
98
106
|
const session = set.sessions[set.activeAgentId];
|
|
99
107
|
const blocker = retryIntentBlocker(store, scoped, retry);
|
|
108
|
+
// Configuration pause neither settles input nor cancels an already
|
|
109
|
+
// admitted attempt. Normal ingress still records its exact terminal.
|
|
110
|
+
if (blocker === "role-disabled")
|
|
111
|
+
continue;
|
|
100
112
|
if (blocker !== undefined && blocker !== "background-work-unsettled") {
|
|
101
113
|
stop(set, blocker, at);
|
|
102
114
|
continue;
|
|
@@ -175,6 +187,10 @@ export function createProviderRetryHooks(home, store, ports = {}) {
|
|
|
175
187
|
|| latest.status !== "waiting")
|
|
176
188
|
return;
|
|
177
189
|
const reason = retryIntentBlocker(store, scoped, latest);
|
|
190
|
+
// Snapshotting yields to configuration changes. Apply the same
|
|
191
|
+
// pause semantics as the pre-await check, without settling intent.
|
|
192
|
+
if (reason === "role-disabled")
|
|
193
|
+
return;
|
|
178
194
|
if (reason !== undefined)
|
|
179
195
|
throw new Error(reason);
|
|
180
196
|
runTaskCommand(["run", "retry", `${owner.taskId}/${previous.id}`], store, {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { ControllerDiagnosticJournal } from "./diagnosticJournal.js";
|
|
3
|
+
import { monotonicMilliseconds } from "../core/controllerTelemetry.js";
|
|
2
4
|
import { prepareNativeHostSteer } from "./nativeHostInput.js";
|
|
3
5
|
import { archiveLeaderTask } from "../task/leaderArchive.js";
|
|
4
6
|
import { TaskWorkspaceCoordinator } from "../repository/taskWorkspaceCoordinator.js";
|
|
7
|
+
import { assertRoleEnabled, RoleDisabledError } from "../role/availability.js";
|
|
5
8
|
import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
|
|
6
9
|
import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
7
10
|
import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveRuntimeHealth, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
|
|
@@ -60,7 +63,6 @@ import { authorizeJobStart } from "./jobControl.js";
|
|
|
60
63
|
import { authorizeGlobalJobStart, createJobDefinitionControl } from "./jobDefinitionControl.js";
|
|
61
64
|
import { reconcileTaskDelegation } from "./jobTaskDelegationControl.js";
|
|
62
65
|
import { createScheduleControl, reconcileSchedules } from "./scheduleControl.js";
|
|
63
|
-
import { reconcileProjectSync } from "../job/projectSync.js";
|
|
64
66
|
import { createFileArtifactPort, createLinuxProcessPort, DurableJobSupervisor } from "./jobSupervisor.js";
|
|
65
67
|
import { createProviderRetryHooks } from "./providerRetryDelivery.js";
|
|
66
68
|
import { createRuntimeResourceActivityTracker } from "./resourceInventory.js";
|
|
@@ -71,8 +73,54 @@ import { AsyncRuntimeEventProcessor, createAsyncRuntimeObserver, FileRuntimeEven
|
|
|
71
73
|
import { RuntimeLaunchCoordinator } from "./runtimeLaunchCoordinator.js";
|
|
72
74
|
import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
|
|
73
75
|
import { readTaskAgentCapabilities } from "./agentCapabilities.js";
|
|
76
|
+
import { workerSchedulerStore } from "./workerSchedulerStore.js";
|
|
77
|
+
import { deserializeError } from "../core/boundedRpc.js";
|
|
74
78
|
/** Production composition root for the current SQLite TaskStore + tmux Controller. */
|
|
75
79
|
export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
80
|
+
const diagnostics = new ControllerDiagnosticJournal(home);
|
|
81
|
+
let timer;
|
|
82
|
+
const closeDiagnostics = async () => {
|
|
83
|
+
if (timer !== undefined)
|
|
84
|
+
clearInterval(timer);
|
|
85
|
+
await diagnostics.close();
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
const running = await createFileTaskControllerRuntime(home, {
|
|
89
|
+
...options,
|
|
90
|
+
diagnosticMetrics: () => diagnostics.metrics(),
|
|
91
|
+
onError: error => { diagnostics.error(error); options.onError?.(error); }
|
|
92
|
+
});
|
|
93
|
+
let expectedAt = monotonicMilliseconds() + 250;
|
|
94
|
+
timer = setInterval(() => {
|
|
95
|
+
const now = monotonicMilliseconds();
|
|
96
|
+
const lagMs = Math.max(0, now - expectedAt);
|
|
97
|
+
expectedAt = now + 250;
|
|
98
|
+
diagnostics.observe({
|
|
99
|
+
...running.runtime.runtimeMetrics(),
|
|
100
|
+
commands: {
|
|
101
|
+
routes: running.server.commandObserver.snapshot(),
|
|
102
|
+
eventLoopDelay: running.server.eventLoopDelay.snapshot()
|
|
103
|
+
}
|
|
104
|
+
}, lagMs);
|
|
105
|
+
}, 250);
|
|
106
|
+
timer.unref();
|
|
107
|
+
const closed = running.closed.finally(closeDiagnostics);
|
|
108
|
+
return { ...running, closed, close: async () => {
|
|
109
|
+
try {
|
|
110
|
+
await running.close();
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await closeDiagnostics();
|
|
114
|
+
}
|
|
115
|
+
} };
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
diagnostics.error(error);
|
|
119
|
+
await closeDiagnostics();
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function createFileTaskControllerRuntime(home, options) {
|
|
76
124
|
// The current Home always uses SQLite. The persistence worker is enabled by
|
|
77
125
|
// default; YUI_STORE_WORKER=0/false keeps the same database in-process.
|
|
78
126
|
const useWorker = resolveStoreWorkerEnabledForHome(home, options.environment ?? process.env);
|
|
@@ -110,6 +158,10 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
110
158
|
closeTelemetry = () => telemetry?.sink.close() ?? Promise.resolve();
|
|
111
159
|
const schedulerStore = options.schedulerStore
|
|
112
160
|
?? new FileSchedulerStoreAdapter(store, telemetry);
|
|
161
|
+
// Injected stores/adapters retain their owner and semantics. Only the
|
|
162
|
+
// production SQLite adapter shares the worker's exact Home and behavior.
|
|
163
|
+
const workerMaintenance = asyncStoreClient !== undefined
|
|
164
|
+
&& options.store === undefined && options.schedulerStore === undefined;
|
|
113
165
|
const domainIdentity = options.domainIdentity
|
|
114
166
|
?? ephemeralDomainFromEnvironment(options.environment ?? process.env);
|
|
115
167
|
const planner = options.planner ?? new FileRoleLaunchPlanner(home, store, {
|
|
@@ -531,7 +583,9 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
531
583
|
const continuationReconciler = options.continuationMetadata === undefined
|
|
532
584
|
? undefined
|
|
533
585
|
: new ProviderContinuationReconciliationService(store, schedulerStore, options.continuationMetadata, options.onError);
|
|
534
|
-
const running = await startFileTaskController(home,
|
|
586
|
+
const running = await startFileTaskController(home, workerMaintenance
|
|
587
|
+
? workerSchedulerStore(schedulerStore, asyncStoreClient)
|
|
588
|
+
: schedulerStore, delivery, (method, params) => method === "task.role-capabilities"
|
|
535
589
|
? readTaskAgentCapabilities(store, catalogs, params)
|
|
536
590
|
: method === "web.start" || method === "web.stop" || method === "web.status"
|
|
537
591
|
? web.dispatch(method, params) : lifecycleDispatcher(method, params), {
|
|
@@ -550,6 +604,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
550
604
|
?? resolveRuntimeHealth(durableConfig.runtimeHealth).diagnosticAfterMs,
|
|
551
605
|
now: options.now,
|
|
552
606
|
onError: options.onError,
|
|
607
|
+
diagnosticMetrics: options.diagnosticMetrics,
|
|
553
608
|
lifecycleHost,
|
|
554
609
|
providerRetry: createProviderRetryHooks(home, store, {
|
|
555
610
|
now: options.now,
|
|
@@ -580,8 +635,12 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
580
635
|
jobDefinitions: createJobDefinitionControl(store, home),
|
|
581
636
|
schedules: {
|
|
582
637
|
control: createScheduleControl(store, home),
|
|
583
|
-
reconcile: now => {
|
|
584
|
-
|
|
638
|
+
reconcile: async (now) => {
|
|
639
|
+
const errors = workerMaintenance
|
|
640
|
+
? await asyncStoreClient.invokeObserver("reconcileProjectSync", [now])
|
|
641
|
+
: schedulerStore.reconcileProjectSync(now);
|
|
642
|
+
for (const error of errors)
|
|
643
|
+
options.onError?.(deserializeError(error));
|
|
585
644
|
reconcileSchedules(store, home, now, options.onError);
|
|
586
645
|
}
|
|
587
646
|
},
|
|
@@ -599,6 +658,10 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
599
658
|
?? (useWorker && asyncStoreClient !== undefined
|
|
600
659
|
? new AsyncRuntimeEventProcessor(runtimeEventInbox, createAsyncRuntimeObserver((method, args) => asyncStoreClient.invokeObserver(method, args)))
|
|
601
660
|
: new FileRuntimeEventProcessor(runtimeEventInbox, schedulerStore)),
|
|
661
|
+
persistenceMetrics: () => ({
|
|
662
|
+
worker: asyncStoreClient?.observationMetrics() ?? null,
|
|
663
|
+
database: store instanceof SqliteTaskStore ? store.storageObservationMetrics() : null
|
|
664
|
+
}),
|
|
602
665
|
runtimeObserver: options.runtimeObserver
|
|
603
666
|
?? new AgentRuntimeObserver(store, runtimeEventInbox),
|
|
604
667
|
domainIdentity,
|
|
@@ -740,6 +803,17 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
740
803
|
});
|
|
741
804
|
}
|
|
742
805
|
catch (error) {
|
|
806
|
+
if (error instanceof RoleDisabledError) {
|
|
807
|
+
const turn = (value.taskId === undefined
|
|
808
|
+
? store.getGlobalRoleSessionSet(value.roleName)
|
|
809
|
+
: store.getTaskRoleSessionSet(value.taskId, value.roleName))?.providerBinding?.run;
|
|
810
|
+
// The admission transaction rolled back. Only a new, unregistered
|
|
811
|
+
// input may use the existing temporary-backpressure wire contract.
|
|
812
|
+
// A matching prior registration remains fenced, never a fresh retry.
|
|
813
|
+
if (turn?.attemptId !== value.attemptId) {
|
|
814
|
+
throw applicationError("SESSION_BUSY", error.message);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
743
817
|
if (error instanceof AgentHostProviderSessionBusyError)
|
|
744
818
|
throw error;
|
|
745
819
|
if (error instanceof AgentHostProviderTurnFenceError) {
|
|
@@ -960,6 +1034,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
960
1034
|
? activeRun?.workspace
|
|
961
1035
|
?? currentDesiredManagedWorkspace(store, request.taskId, request.roleName)
|
|
962
1036
|
: undefined;
|
|
1037
|
+
assertRoleEnabled(role);
|
|
963
1038
|
// Only Draft planning may launch without an adopted delivery workspace.
|
|
964
1039
|
const workspaceFree = request.scope === "task"
|
|
965
1040
|
&& managedWorkspace === undefined
|
|
@@ -1082,6 +1157,7 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
|
1082
1157
|
if (role === null) {
|
|
1083
1158
|
throw new Error(`Role no longer exists: ${request.owner.roleName}.`);
|
|
1084
1159
|
}
|
|
1160
|
+
assertRoleEnabled(role);
|
|
1085
1161
|
const sessions = request.owner.scope === "task"
|
|
1086
1162
|
? store.getTaskRoleSessionSet(request.owner.taskId, request.owner.roleName)
|
|
1087
1163
|
: store.getGlobalRoleSessionSet(request.owner.roleName);
|
|
@@ -92,7 +92,9 @@ export class RuntimeLaunchCoordinator {
|
|
|
92
92
|
...(request.runId === undefined ? {} : { runId: request.runId }),
|
|
93
93
|
...(request.environment === undefined ? {} : { environment: request.environment })
|
|
94
94
|
};
|
|
95
|
-
|
|
95
|
+
// Even without a startup-facts consumer, recheck after asynchronous Host
|
|
96
|
+
// validation and immediately before creating the process.
|
|
97
|
+
const callback = beforeHostStart === undefined ? () => assertLaunchCurrent() : observePreflight;
|
|
96
98
|
if (request.mode === "resume" && !request.nativeSessionId) {
|
|
97
99
|
throw new Error("Restoring a Session requires its native session id.");
|
|
98
100
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep maintenance read/check/write closures together on the persistence
|
|
3
|
+
* worker. These operations recheck their exact durable condition in one
|
|
4
|
+
* transaction and are idempotent under the worker's existing replay contract.
|
|
5
|
+
* Do not add admission/claim operations here: their result and unknown-effect
|
|
6
|
+
* contracts need a separate audit before moving across this boundary.
|
|
7
|
+
*/
|
|
8
|
+
const WORKER_MAINTENANCE = new Set([
|
|
9
|
+
"resolveExpiredInputRecommendations",
|
|
10
|
+
"recordRoleRunProgress",
|
|
11
|
+
"recordRoleRunDiagnostic",
|
|
12
|
+
"recordRoleRunStall"
|
|
13
|
+
]);
|
|
14
|
+
export function workerSchedulerStore(store, client) {
|
|
15
|
+
return new Proxy(store, {
|
|
16
|
+
get(target, property) {
|
|
17
|
+
if (WORKER_MAINTENANCE.has(property)) {
|
|
18
|
+
return (...args) => client.invokeObserver(String(property), args);
|
|
19
|
+
}
|
|
20
|
+
const value = Reflect.get(target, property);
|
|
21
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
package/dist/core/boundedRpc.js
CHANGED
|
@@ -48,6 +48,10 @@ export function serializeError(error) {
|
|
|
48
48
|
export function deserializeError(serialized) {
|
|
49
49
|
const error = new Error(serialized.message);
|
|
50
50
|
error.name = serialized.name;
|
|
51
|
+
if (serialized.code !== undefined)
|
|
52
|
+
Object.assign(error, { code: serialized.code });
|
|
53
|
+
if (serialized.stack !== undefined)
|
|
54
|
+
error.stack = serialized.stack;
|
|
51
55
|
return error;
|
|
52
56
|
}
|
|
53
57
|
// -- Bounded slot pool (backpressure) ----------------------------------------
|
|
@@ -311,7 +311,7 @@ async function routeRequest(socket, line, token, dispatcher, stop, status, telem
|
|
|
311
311
|
// observation completes when the response is handed to the socket, so a
|
|
312
312
|
// later status snapshot sees prior built-in completions and the dispatcher
|
|
313
313
|
// service-time metric stays a distinct observation.
|
|
314
|
-
const observation = telemetry.commandObserver.start();
|
|
314
|
+
const observation = telemetry.commandObserver.start(request.method, request.id);
|
|
315
315
|
const send = (response, onFlushed) => {
|
|
316
316
|
observation.complete(isBuiltinControllerMethod(request.method) ? "builtin" : "dispatched", response.ok ? "success" : "failure");
|
|
317
317
|
sendResponse(socket, response, onFlushed);
|
|
@@ -1,13 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** Fixed-size retained diagnostics; live bookkeeping follows in-flight work.
|
|
3
|
+
* Not a log or scheduling authority. No params,
|
|
4
|
+
* prompts, paths, failure text or credentials are retained. Target/request
|
|
5
|
+
* hashes correlate an observation without publishing arbitrary input. */
|
|
6
|
+
export class ControllerWorkObserver {
|
|
7
|
+
clock;
|
|
8
|
+
#active = new Map();
|
|
9
|
+
#recent = [];
|
|
10
|
+
#completed = 0;
|
|
11
|
+
#maximumMs = 0;
|
|
12
|
+
#slowest = null;
|
|
13
|
+
constructor(clock = monotonicMilliseconds) {
|
|
14
|
+
this.clock = clock;
|
|
15
|
+
}
|
|
16
|
+
start(phase, target) {
|
|
17
|
+
const token = {};
|
|
18
|
+
const entry = { phase, target: target === undefined ? null
|
|
19
|
+
: createHash("sha256").update(target).digest("hex").slice(0, 16), startedAt: this.clock() };
|
|
20
|
+
this.#active.set(token, entry);
|
|
21
|
+
return (failed = false) => {
|
|
22
|
+
if (!this.#active.delete(token))
|
|
23
|
+
return;
|
|
24
|
+
const elapsedMs = Math.max(0, this.clock() - entry.startedAt);
|
|
25
|
+
this.#completed++;
|
|
26
|
+
this.#maximumMs = Math.max(this.#maximumMs, elapsedMs);
|
|
27
|
+
const completed = { phase, target: entry.target, startedAtMs: entry.startedAt, elapsedMs, failed };
|
|
28
|
+
if (this.#slowest === null || elapsedMs > this.#slowest.elapsedMs)
|
|
29
|
+
this.#slowest = completed;
|
|
30
|
+
this.#recent.push(completed);
|
|
31
|
+
if (this.#recent.length > 16)
|
|
32
|
+
this.#recent.shift();
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async observe(phase, target, action) {
|
|
36
|
+
const finish = this.start(phase, target);
|
|
37
|
+
try {
|
|
38
|
+
const value = await action();
|
|
39
|
+
finish();
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
finish(true);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
snapshot() {
|
|
48
|
+
const active = [];
|
|
49
|
+
for (const entry of this.#active.values()) {
|
|
50
|
+
active.push({ phase: entry.phase, target: entry.target, startedAtMs: entry.startedAt,
|
|
51
|
+
ageMs: Math.max(0, this.clock() - entry.startedAt) });
|
|
52
|
+
if (active.length === 16)
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
completed: this.#completed, maximumMs: this.#maximumMs, inFlight: this.#active.size,
|
|
57
|
+
active, slowest: this.#slowest,
|
|
58
|
+
recent: [...this.#recent]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Small worker-response observation; no historical arrays on the RPC wire. */
|
|
63
|
+
export function compactWorkObservation(observer) {
|
|
64
|
+
const { completed, maximumMs, slowest, recent } = observer.snapshot();
|
|
65
|
+
return { completed, maximumMs, slowest, latest: recent.at(-1) ?? null };
|
|
66
|
+
}
|
|
11
67
|
const CONTROLLER_DELAY_BUCKETS_MS = [10, 50, 100, 250, 500, 1_000, 3_000];
|
|
12
68
|
const DEFAULT_EVENT_LOOP_DELAY_INTERVAL_MS = 50;
|
|
13
69
|
const BUILTIN_METHODS = new Set([
|
|
@@ -19,6 +75,12 @@ const BUILTIN_METHODS = new Set([
|
|
|
19
75
|
"controller.rollback-handover",
|
|
20
76
|
"controller.handover-state"
|
|
21
77
|
]);
|
|
78
|
+
const OBSERVED_METHODS = new Set([...BUILTIN_METHODS,
|
|
79
|
+
"scheduler.signal", "scheduler.scan", "scheduler.configure",
|
|
80
|
+
"runtime.ensure-role-session", "runtime.stop-role-session",
|
|
81
|
+
"capability.search", "capability.describe", "capability.call",
|
|
82
|
+
"schedule.control", "job.definition", "job.start", "job.get", "job.cancel", "job.acknowledge"
|
|
83
|
+
]);
|
|
22
84
|
export function isBuiltinControllerMethod(method) {
|
|
23
85
|
return BUILTIN_METHODS.has(method);
|
|
24
86
|
}
|
|
@@ -47,19 +109,23 @@ export class ControllerCommandObserver {
|
|
|
47
109
|
#completed = 0;
|
|
48
110
|
#failed = 0;
|
|
49
111
|
#inFlight = 0;
|
|
50
|
-
/**
|
|
51
|
-
#inFlightStarted =
|
|
112
|
+
/** Insertion order preserves arrival order; completion removes its own token. */
|
|
113
|
+
#inFlightStarted = new Map();
|
|
52
114
|
#builtinCompleted = 0;
|
|
53
115
|
#builtinFailed = 0;
|
|
54
116
|
#dispatchedCompleted = 0;
|
|
55
117
|
#dispatchedFailed = 0;
|
|
118
|
+
#observations;
|
|
56
119
|
constructor(clock = monotonicMilliseconds) {
|
|
57
120
|
this.#clock = clock;
|
|
121
|
+
this.#observations = new ControllerWorkObserver(clock);
|
|
58
122
|
}
|
|
59
|
-
start() {
|
|
123
|
+
start(method = "unknown", requestId) {
|
|
60
124
|
this.#received += 1;
|
|
61
125
|
this.#inFlight += 1;
|
|
62
|
-
|
|
126
|
+
const token = {};
|
|
127
|
+
this.#inFlightStarted.set(token, this.#clock());
|
|
128
|
+
const finish = this.#observations.start(OBSERVED_METHODS.has(method) ? method : "other", requestId);
|
|
63
129
|
let completed = false;
|
|
64
130
|
return {
|
|
65
131
|
complete: (kind, outcome) => {
|
|
@@ -67,7 +133,8 @@ export class ControllerCommandObserver {
|
|
|
67
133
|
return;
|
|
68
134
|
completed = true;
|
|
69
135
|
this.#inFlight = Math.max(0, this.#inFlight - 1);
|
|
70
|
-
this.#inFlightStarted.
|
|
136
|
+
this.#inFlightStarted.delete(token);
|
|
137
|
+
finish(outcome === "failure");
|
|
71
138
|
this.#completed += 1;
|
|
72
139
|
if (outcome === "failure")
|
|
73
140
|
this.#failed += 1;
|
|
@@ -87,9 +154,9 @@ export class ControllerCommandObserver {
|
|
|
87
154
|
};
|
|
88
155
|
}
|
|
89
156
|
snapshot() {
|
|
90
|
-
const oldest = this.#inFlightStarted.
|
|
157
|
+
const oldest = this.#inFlightStarted.size === 0
|
|
91
158
|
? null
|
|
92
|
-
: Math.max(0, this.#clock() - this.#inFlightStarted
|
|
159
|
+
: Math.max(0, this.#clock() - this.#inFlightStarted.values().next().value);
|
|
93
160
|
return {
|
|
94
161
|
received: this.#received,
|
|
95
162
|
completed: this.#completed,
|
|
@@ -103,7 +170,8 @@ export class ControllerCommandObserver {
|
|
|
103
170
|
dispatched: {
|
|
104
171
|
completed: this.#dispatchedCompleted,
|
|
105
172
|
failed: this.#dispatchedFailed
|
|
106
|
-
}
|
|
173
|
+
},
|
|
174
|
+
observations: this.#observations.snapshot()
|
|
107
175
|
};
|
|
108
176
|
}
|
|
109
177
|
}
|
|
@@ -122,6 +190,7 @@ export class ControllerEventLoopDelay {
|
|
|
122
190
|
#expectedAt = 0;
|
|
123
191
|
#samples = 0;
|
|
124
192
|
#maximumLagMs = 0;
|
|
193
|
+
#maximumObservedAtMs = null;
|
|
125
194
|
#buckets = new Map(CONTROLLER_DELAY_BUCKETS_MS.map((threshold) => [threshold, 0]));
|
|
126
195
|
#stopped = false;
|
|
127
196
|
constructor(clock = monotonicMilliseconds, scheduler = productionTelemetryScheduler, intervalMs = DEFAULT_EVENT_LOOP_DELAY_INTERVAL_MS) {
|
|
@@ -142,7 +211,10 @@ export class ControllerEventLoopDelay {
|
|
|
142
211
|
this.#expectedAt = this.#clock() + this.#intervalMs;
|
|
143
212
|
this.#samples += 1;
|
|
144
213
|
const bounded = Math.ceil(lag);
|
|
145
|
-
this.#
|
|
214
|
+
if (this.#maximumObservedAtMs === null || bounded > this.#maximumLagMs) {
|
|
215
|
+
this.#maximumLagMs = bounded;
|
|
216
|
+
this.#maximumObservedAtMs = this.#clock();
|
|
217
|
+
}
|
|
146
218
|
for (const threshold of CONTROLLER_DELAY_BUCKETS_MS) {
|
|
147
219
|
if (bounded <= threshold) {
|
|
148
220
|
this.#buckets.set(threshold, (this.#buckets.get(threshold) ?? 0) + 1);
|
|
@@ -158,6 +230,7 @@ export class ControllerEventLoopDelay {
|
|
|
158
230
|
return {
|
|
159
231
|
samples: this.#samples,
|
|
160
232
|
maximumLagMs: this.#maximumLagMs,
|
|
233
|
+
maximumObservedAtMs: this.#maximumObservedAtMs,
|
|
161
234
|
lagBuckets: Object.fromEntries(CONTROLLER_DELAY_BUCKETS_MS.map((threshold) => [
|
|
162
235
|
`le${threshold}ms`,
|
|
163
236
|
this.#buckets.get(threshold) ?? 0
|