@zq-silk/yui 2.21.0 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/cli/commandCatalog.js +87 -27
- package/dist/cli/dynamicCompletion.js +4 -1
- package/dist/cli/helpRenderer.js +12 -1
- package/dist/cli/interactionPolicy.js +2 -0
- package/dist/cli/invocationRouter.js +5 -5
- package/dist/cli/roleWizard.js +5 -1
- package/dist/cli/updateCommand.js +20 -3
- package/dist/cli/updateOrchestrator.js +13 -0
- package/dist/cli/updatePorts.js +27 -0
- package/dist/commands/globalRoleCommands.js +33 -8
- package/dist/commands/inboxCommands.js +39 -12
- package/dist/commands/operatorCommands.js +2 -5
- package/dist/commands/releaseCommands.js +16 -2
- package/dist/commands/roleConfiguration.js +25 -2
- package/dist/commands/sessionCommands.js +5 -2
- package/dist/commands/taskCommands.js +217 -64
- package/dist/commands/taskContextCommand.js +26 -4
- package/dist/commands/taskFactCommands.js +15 -2
- package/dist/commands/taskIntegrationCommands.js +3 -3
- package/dist/commands/taskRoleRuntimeStatus.js +19 -5
- package/dist/commands/textInput.js +2 -2
- package/dist/context/contextFamilies.js +10 -0
- package/dist/context/currentSkills.js +37 -4
- package/dist/context/runContextPack.js +44 -5
- package/dist/context/runInputContract.js +1 -1
- package/dist/context/sessionBootstrapManifest.js +21 -6
- package/dist/context/sessionSkillNotice.js +74 -0
- package/dist/context/taskContext.js +100 -16
- package/dist/controlPlaneCli.js +17 -7
- package/dist/controller/agentRuntimeObserver.js +57 -12
- package/dist/controller/clientRuntime.js +66 -18
- package/dist/controller/controller.js +19 -2
- package/dist/controller/diagnosticJournal.js +10 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +74 -24
- package/dist/controller/hostLifecycle.js +153 -0
- package/dist/controller/hostLifecycleChecks.js +27 -0
- package/dist/controller/hostUpgrade.js +344 -0
- package/dist/controller/messageHealth.js +4 -3
- package/dist/controller/providerRetryAdmission.js +15 -1
- package/dist/controller/providerRetryDelivery.js +18 -2
- package/dist/controller/runtime.js +82 -21
- package/dist/controller/runtimeEventInbox.js +107 -13
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/controller/sessionNotify.js +5 -46
- package/dist/controller/taskRunRecovery.js +153 -0
- package/dist/core/controllerClient.js +16 -2
- package/dist/core/controllerServer.js +14 -5
- package/dist/core/controllerTelemetry.js +51 -5
- package/dist/errors/cliFailure.js +14 -5
- package/dist/execution/codexThreadNaming.js +54 -43
- package/dist/executor/agentAdapter.js +32 -4
- package/dist/executor/agentConfigurationFields.js +2 -0
- package/dist/executor/fileRoleLaunchPlanner.js +18 -3
- package/dist/integration/gitIntegrationService.js +3 -2
- package/dist/integration/integrationSourceApplication.js +12 -1
- package/dist/interaction/notificationPresentation.js +11 -16
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/message/messageContinuation.js +3 -1
- package/dist/output/boundedRead.js +36 -7
- package/dist/output/rolePresentation.js +3 -1
- package/dist/runtime/agentEndpoint.js +11 -1
- package/dist/runtime/agentEndpointIdentity.js +2 -0
- package/dist/runtime/agentHost.js +174 -40
- package/dist/runtime/agentHostImplementation.js +3 -1
- package/dist/runtime/codexAppServerRuntime.js +113 -24
- package/dist/runtime/codexInteractiveHost.js +102 -11
- package/dist/runtime/codexNativeAccess.js +1 -1
- package/dist/runtime/contextRecovery.js +15 -0
- package/dist/runtime/hostSessionLifecycle.js +83 -0
- package/dist/runtime/hostUpgradeGate.js +43 -0
- package/dist/runtime/lifecycleChecks.js +47 -0
- package/dist/runtime/lifecycleReservation.js +9 -1
- package/dist/runtime/nativeTitleReceipt.js +31 -0
- package/dist/runtime/nativeTitleSync.js +192 -0
- package/dist/runtime/providerRetry.js +9 -6
- package/dist/runtime/providerRuntimeIdentity.js +1 -1
- package/dist/runtime/structuredProviderHost.js +65 -35
- package/dist/scheduler/wakeupQueue.js +10 -4
- package/dist/storage/sqliteSchema.js +29 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/tmux/tmuxManager.js +4 -2
- package/dist/web/webSettings.js +2 -0
- package/docs/authorization.md +18 -3
- package/docs/cli-information-contract.md +228 -6
- package/docs/cli-information-contract.zh-CN.md +95 -5
- package/docs/managed-turn-and-session-runtime.md +177 -21
- package/docs/managed-turn-and-session-runtime.zh-CN.md +126 -14
- package/docs/provider-retry.md +11 -0
- package/docs/release-workflow.md +65 -0
- package/docs/release-workflow.zh-CN.md +48 -0
- package/docs/roles-and-configuration.md +28 -0
- package/docs/roles-and-configuration.zh-CN.md +21 -0
- package/docs/storage-baseline.md +13 -1
- package/docs/storage-baseline.zh-CN.md +5 -1
- package/docs/task-delivery.md +7 -0
- package/docs/task-delivery.zh-CN.md +5 -0
- package/docs/testing/review-evaluation.md +7 -7
- package/docs/testing/review-evaluation.zh-CN.md +7 -7
- package/docs/testing/verification-levels.md +45 -2
- package/docs/testing/verification-levels.zh-CN.md +21 -2
- package/i18n/README.zh-CN.md +6 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +98 -193
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/artifacts.md +41 -0
- package/skills/yui-leader/references/assignment-execution.md +140 -0
- package/skills/yui-leader/references/completion.md +79 -0
- package/skills/yui-leader/references/delivery-scope.md +26 -0
- package/skills/yui-leader/references/execution.md +16 -487
- package/skills/yui-leader/references/implementation.md +81 -0
- package/skills/yui-leader/references/planning.md +7 -45
- package/skills/yui-leader/references/review.md +90 -0
- package/skills/yui-leader/references/task-progress.md +23 -0
- package/skills/yui-leader/references/work-allocation.md +118 -0
- package/skills/yui-operator/SKILL.md +77 -333
- package/skills/yui-operator/VERSION +1 -1
- package/skills/yui-operator/references/body-transport.md +62 -0
- package/skills/yui-operator/references/configuration.md +47 -0
- package/skills/yui-operator/references/lifecycle.md +41 -0
- package/skills/yui-operator/references/progress.md +39 -0
- package/skills/yui-operator/references/routing.md +57 -0
- package/skills/yui-operator/references/submission.md +56 -0
- package/skills/yui-operator/references/task-delivery.md +1 -1
- package/skills/yui-operator/references/web-lifecycle.md +19 -0
- package/skills/yui-operator/scripts/verify-submission.mjs +37 -0
- package/skills/yui-runtime/SKILL.md +182 -531
- package/skills/yui-runtime/VERSION +1 -1
- package/skills/yui-runtime/references/archive.md +51 -0
- package/skills/yui-runtime/references/command-discovery.md +41 -0
- package/skills/yui-runtime/references/completed-followup.md +65 -0
- package/skills/yui-runtime/references/context-ordering.md +3 -34
- package/skills/yui-runtime/references/context-reads.md +61 -0
- package/skills/yui-runtime/references/dispatched-run.md +61 -0
- package/skills/yui-runtime/references/execution-failure.md +105 -0
- package/skills/yui-runtime/references/global-context.md +18 -0
- package/skills/yui-runtime/references/global-input-controls.md +44 -0
- package/skills/yui-runtime/references/host-custody.md +30 -0
- package/skills/yui-runtime/references/inbox-recovery.md +77 -0
- package/skills/yui-runtime/references/incremental-context.md +40 -0
- package/skills/yui-runtime/references/publication.md +4 -111
- package/skills/yui-runtime/references/recovery.md +32 -192
- package/skills/yui-runtime/references/session-identity.md +18 -0
- package/skills/yui-runtime/references/session-replacement.md +45 -0
- package/skills/yui-runtime/references/uncertain-execution.md +29 -0
package/dist/controlPlaneCli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { readFileSync, realpathSync } from "node:fs";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { isatty } from "node:tty";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { isDeepStrictEqual } from "node:util";
|
|
9
10
|
import { agentAdapterLabel as adapterLabel } from "./agent/adapterCatalog.js";
|
|
@@ -11,14 +12,14 @@ import { recordTaskInterruptResult } from "./message/taskInterrupt.js";
|
|
|
11
12
|
import { nativeAgentEnvironmentNames } from "./agent/launchEnvironment.js";
|
|
12
13
|
import { listArtifactsCapability, readArtifactCapability, saveArtifactCapability } from "./artifacts/artifactCapability.js";
|
|
13
14
|
import { renderAgentConfigurationResolutionNotice } from "./cli/agentConfigurationPicker.js";
|
|
14
|
-
import { describeCommandTree, findCommandNode } from "./cli/commandCatalog.js";
|
|
15
|
+
import { describeCommandHelp, describeCommandTree, findCommandNode } from "./cli/commandCatalog.js";
|
|
15
16
|
import { discoveryCommandTree } from "./cli/commandDiscovery.js";
|
|
16
17
|
import { callerDiscoveryAudience } from "./cli/callerDiscovery.js";
|
|
17
18
|
import { assertTaskReadable } from "./context/taskContext.js";
|
|
18
19
|
import { renderCompletion } from "./cli/completion.js";
|
|
19
20
|
import { runCompletionWizard } from "./cli/completionWizard.js";
|
|
20
21
|
import { resolveCompletionCandidates } from "./cli/dynamicCompletion.js";
|
|
21
|
-
import { renderCommandHelp } from "./cli/helpRenderer.js";
|
|
22
|
+
import { renderCommandHelp, renderCommandPathHint } from "./cli/helpRenderer.js";
|
|
22
23
|
import { allowsInteractiveSelection, resolveInteractiveArguments } from "./cli/interactiveSelection.js";
|
|
23
24
|
import { routeInvocation } from "./cli/invocationRouter.js";
|
|
24
25
|
import { operatorOfflineCommand, isTaskReadCommand } from "./cli/managedDiagnostics.js";
|
|
@@ -202,7 +203,7 @@ export async function main() {
|
|
|
202
203
|
return;
|
|
203
204
|
}
|
|
204
205
|
if (args.length === 0) {
|
|
205
|
-
emit(renderCommandHelp(discovery, VERSION), false, jsonOutput ?
|
|
206
|
+
emit(renderCommandHelp(discovery, VERSION), false, jsonOutput ? describeCommandHelp(discovery) : undefined);
|
|
206
207
|
return;
|
|
207
208
|
}
|
|
208
209
|
if (args[0] === "version" && args.length === 1) {
|
|
@@ -211,11 +212,19 @@ export async function main() {
|
|
|
211
212
|
}
|
|
212
213
|
const invocation = routeInvocation(args, discovery);
|
|
213
214
|
if (invocation.kind === "help") {
|
|
214
|
-
|
|
215
|
+
if (invocation.recursive && !jsonOutput)
|
|
216
|
+
throw usageError("Recursive help requires --json.");
|
|
217
|
+
emit(renderCommandHelp(invocation.node, VERSION), true, jsonOutput
|
|
218
|
+
? invocation.recursive ? describeCommandTree(invocation.node) : describeCommandHelp(invocation.node)
|
|
219
|
+
: undefined);
|
|
215
220
|
return;
|
|
216
221
|
}
|
|
217
222
|
if (invocation.kind === "path-error") {
|
|
218
|
-
throw usageError(
|
|
223
|
+
throw usageError(invocation.discoveryOnly
|
|
224
|
+
? `Command not listed in the current caller's help view: ${invocation.typedPath}. `
|
|
225
|
+
+ "Help is filtered discovery, not an execution or permission check. "
|
|
226
|
+
+ "Follow current instructions and exact read links; execution still validates authority."
|
|
227
|
+
: `Unknown command: ${invocation.typedPath}`, renderCommandPathHint(invocation.helpNode));
|
|
219
228
|
}
|
|
220
229
|
if (invocation.kind === "incomplete") {
|
|
221
230
|
throw usageError(`Command required after: ${invocation.typedPath}`, renderCommandHelp(invocation.helpNode, VERSION));
|
|
@@ -660,7 +669,6 @@ export async function main() {
|
|
|
660
669
|
const tmux = new TmuxManager(resolveTmuxBin(store.getConfig().tmuxBin), executor, {
|
|
661
670
|
yuiHome: home,
|
|
662
671
|
historyLimit: resolveTmuxHistoryLimit(store.getConfig().tmuxHistoryLimit),
|
|
663
|
-
terminalInput: process.stdin,
|
|
664
672
|
onWarning: (message) => writeCliWarning(message)
|
|
665
673
|
});
|
|
666
674
|
const telemetry = openSchedulerTelemetry(home, store.getConfig());
|
|
@@ -2750,7 +2758,9 @@ function completionShell(value) {
|
|
|
2750
2758
|
throw usageError("Completion shell must be one of bash, zsh, fish.");
|
|
2751
2759
|
}
|
|
2752
2760
|
async function resolveTerminalArguments(commandArgs, node, store, catalogs) {
|
|
2753
|
-
|
|
2761
|
+
// Probe the descriptor without constructing a stdin stream: Node makes
|
|
2762
|
+
// pipes nonblocking on stream creation, breaking later --body-file - reads.
|
|
2763
|
+
const interactive = isatty(0) && process.stdout.isTTY === true;
|
|
2754
2764
|
if (!interactive || !allowsInteractiveSelection(commandArgs, jsonOutput)) {
|
|
2755
2765
|
return [...commandArgs];
|
|
2756
2766
|
}
|
|
@@ -15,14 +15,45 @@ export class AgentRuntimeObserver {
|
|
|
15
15
|
#states = new Map();
|
|
16
16
|
#maxConcurrentSamples;
|
|
17
17
|
#sequence = 0;
|
|
18
|
+
#samples = 0;
|
|
19
|
+
#sources = 0;
|
|
20
|
+
#published = 0;
|
|
21
|
+
#usagePublications = 0;
|
|
22
|
+
#failed = 0;
|
|
18
23
|
constructor(store, inbox, drivers = builtinAgentDriverRegistry(), options = {}) {
|
|
19
24
|
this.store = store;
|
|
20
25
|
this.inbox = inbox;
|
|
21
26
|
this.drivers = drivers;
|
|
22
27
|
this.#maxConcurrentSamples = sampleConcurrency(options.maxConcurrentSamples);
|
|
23
28
|
}
|
|
24
|
-
async sample(now = new Date()) {
|
|
25
|
-
|
|
29
|
+
async sample(now = new Date(), phases) {
|
|
30
|
+
this.#samples++;
|
|
31
|
+
const finishDiscovery = phases?.start("runtime-sample-discovery", "home");
|
|
32
|
+
let active;
|
|
33
|
+
try {
|
|
34
|
+
active = this.activeSources();
|
|
35
|
+
finishDiscovery?.();
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
finishDiscovery?.(true);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
this.#sources += active.length;
|
|
42
|
+
const publish = async (observation) => {
|
|
43
|
+
try {
|
|
44
|
+
const result = await (phases === undefined
|
|
45
|
+
? this.inbox.enqueueObservationAsync(observation)
|
|
46
|
+
: phases.observe(observation.payload.usage === undefined ? "runtime-sample-publish" : "runtime-usage-publish", undefined, () => this.inbox.enqueueObservationAsync(observation)));
|
|
47
|
+
this.#published++;
|
|
48
|
+
if (observation.payload.usage !== undefined)
|
|
49
|
+
this.#usagePublications++;
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
this.#failed++;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
26
57
|
const activeKeys = new Set(active.map(({ key }) => key));
|
|
27
58
|
for (const key of this.#states.keys()) {
|
|
28
59
|
if (!activeKeys.has(key))
|
|
@@ -37,7 +68,10 @@ export class AgentRuntimeObserver {
|
|
|
37
68
|
// usage/activity baseline is durable. Rehydrate it after Controller
|
|
38
69
|
// restart so rereading the bounded transcript tail cannot manufacture a
|
|
39
70
|
// fresh activity edge from tokens that were already observed.
|
|
40
|
-
|
|
71
|
+
// Commit process-local state only when every fact from this sample is
|
|
72
|
+
// durable. A failed later publication must leave the old cursor usable;
|
|
73
|
+
// retry deduplicates any already-persisted prefix.
|
|
74
|
+
const state = { ...(existingState ?? persistedState) };
|
|
41
75
|
const restoredUsageEventId = existingState === undefined
|
|
42
76
|
? persistedState.usageEventId
|
|
43
77
|
: undefined;
|
|
@@ -63,7 +97,6 @@ export class AgentRuntimeObserver {
|
|
|
63
97
|
detail: error instanceof Error ? error.message : String(error)
|
|
64
98
|
});
|
|
65
99
|
}
|
|
66
|
-
state.cursor = sample.cursor;
|
|
67
100
|
const at = now.toISOString();
|
|
68
101
|
// Reserve sequence numbers in source-key order before sampling. Provider
|
|
69
102
|
// latency can change completion order without changing observation
|
|
@@ -101,7 +134,7 @@ export class AgentRuntimeObserver {
|
|
|
101
134
|
const identity = completeHistory
|
|
102
135
|
? tokenObservationIdentity("baseline", fence, source.sourceId, baselineKey)
|
|
103
136
|
: usageObservationIdentity(fence, source.sourceId, latestOccurrence);
|
|
104
|
-
|
|
137
|
+
await publish(createRuntimeObservation({
|
|
105
138
|
schemaVersion: 1,
|
|
106
139
|
eventId: identity.eventId,
|
|
107
140
|
semanticKey: identity.semanticKey,
|
|
@@ -136,7 +169,7 @@ export class AgentRuntimeObserver {
|
|
|
136
169
|
}
|
|
137
170
|
const health = JSON.stringify([sample.status, sample.detail ?? null]);
|
|
138
171
|
if (state.health !== health) {
|
|
139
|
-
|
|
172
|
+
await publish(createRuntimeObservation({
|
|
140
173
|
schemaVersion: 1,
|
|
141
174
|
eventId: observationId("health", fence, source.sourceId, health),
|
|
142
175
|
semanticKey: observationId("health", fence, source.sourceId, health),
|
|
@@ -180,9 +213,9 @@ export class AgentRuntimeObserver {
|
|
|
180
213
|
: Object.freeze({ ...occurrence, observationQuality: "partial" })));
|
|
181
214
|
}
|
|
182
215
|
}
|
|
183
|
-
usages.
|
|
216
|
+
for (const [usageIndex, occurrence] of usages.entries()) {
|
|
184
217
|
const identity = usageObservationIdentity(fence, source.sourceId, occurrence);
|
|
185
|
-
|
|
218
|
+
await publish(createRuntimeObservation({
|
|
186
219
|
schemaVersion: 1,
|
|
187
220
|
eventId: identity.eventId,
|
|
188
221
|
semanticKey: identity.semanticKey,
|
|
@@ -210,9 +243,9 @@ export class AgentRuntimeObserver {
|
|
|
210
243
|
state.usageOccurrenceId = occurrence.occurrenceId;
|
|
211
244
|
state.usageOccurrenceCheckpoint = occurrence.resumeCheckpoint;
|
|
212
245
|
}
|
|
213
|
-
}
|
|
214
|
-
if (activityChanged &&
|
|
215
|
-
|
|
246
|
+
}
|
|
247
|
+
if (activityChanged && sample.cursor !== undefined) {
|
|
248
|
+
await publish(createRuntimeObservation({
|
|
216
249
|
schemaVersion: 1,
|
|
217
250
|
eventId: observationId("activity", fence, source.sourceId, sample.activityId),
|
|
218
251
|
semanticKey: observationId("activity", fence, source.sourceId, sample.activityId),
|
|
@@ -232,10 +265,17 @@ export class AgentRuntimeObserver {
|
|
|
232
265
|
}
|
|
233
266
|
if (sample.activityId !== undefined)
|
|
234
267
|
state.activityId = sample.activityId;
|
|
268
|
+
state.cursor = sample.cursor;
|
|
235
269
|
this.#states.set(key, state);
|
|
236
270
|
});
|
|
237
271
|
return Object.freeze([...dirty].sort(numericCompare));
|
|
238
272
|
}
|
|
273
|
+
/** Counts successful durable admissions (including duplicates), not tokens or
|
|
274
|
+
* provider content. Phase observations carry discovery/publication duration. */
|
|
275
|
+
metrics() {
|
|
276
|
+
return { samples: this.#samples, sources: this.#sources, published: this.#published,
|
|
277
|
+
usagePublications: this.#usagePublications, failed: this.#failed };
|
|
278
|
+
}
|
|
239
279
|
activeSources() {
|
|
240
280
|
const result = [];
|
|
241
281
|
const activeTasks = [...new Set(this.store.listActiveTaskIds())]
|
|
@@ -354,13 +394,18 @@ export class AgentRuntimeObserver {
|
|
|
354
394
|
async function forEachConcurrent(values, limit, visit) {
|
|
355
395
|
let nextIndex = 0;
|
|
356
396
|
const workers = Math.min(limit, values.length);
|
|
357
|
-
await Promise.
|
|
397
|
+
const results = await Promise.allSettled(Array.from({ length: workers }, async () => {
|
|
358
398
|
while (nextIndex < values.length) {
|
|
359
399
|
const index = nextIndex;
|
|
360
400
|
nextIndex += 1;
|
|
361
401
|
await visit(values[index], index);
|
|
362
402
|
}
|
|
363
403
|
}));
|
|
404
|
+
// An admission failure must not release the Controller's in-flight sample
|
|
405
|
+
// guard while another source can still commit state from that sample.
|
|
406
|
+
const failure = results.find(result => result.status === "rejected");
|
|
407
|
+
if (failure?.status === "rejected")
|
|
408
|
+
throw failure.reason;
|
|
364
409
|
}
|
|
365
410
|
function sampleConcurrency(value) {
|
|
366
411
|
const resolved = value ?? DEFAULT_MAX_CONCURRENT_SAMPLES;
|
|
@@ -2,7 +2,8 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { jobCleanupBlocker } from "../job/jobCleanupInspection.js";
|
|
3
3
|
import { describeCliFailure } from "../errors/cliFailure.js";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { findLiveControllerProcessForHome
|
|
5
|
+
import { findLiveControllerProcessForHome } from "../core/controllerProcessIdentity.js";
|
|
6
|
+
import { processGenerationIsLive } from "../core/fileLockOwner.js";
|
|
6
7
|
import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
|
|
7
8
|
import { parseControllerIdentity } from "../core/controllerIdentity.js";
|
|
8
9
|
import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, operationalAgentEnvironment, selectEnvironment, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
|
|
@@ -12,6 +13,7 @@ import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
|
12
13
|
import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
|
|
13
14
|
import { hasRuntimeLifecycleWork } from "../runtime/lifecycleReservation.js";
|
|
14
15
|
import { assertControllerStatusIdentity } from "../runtime/runtimeCoherence.js";
|
|
16
|
+
import { readLinuxProcessIdentity } from "../runtime/sessionOwnerIdentity.js";
|
|
15
17
|
import { CONTROLLER_SHUTDOWN_TIMEOUT_MS, LIFECYCLE_REQUEST_TIMEOUT_MS } from "../runtime/runtimeDeadlines.js";
|
|
16
18
|
import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
17
19
|
import { yuiVersionIdentity } from "../version.js";
|
|
@@ -57,21 +59,31 @@ export async function assertFileTaskControllerStorageCompatible(home, options =
|
|
|
57
59
|
export async function ensureFileTaskController(home, options = {}) {
|
|
58
60
|
const call = options.call ?? callController;
|
|
59
61
|
await waitForForeignHandover(home, options);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
const timeoutMs = positive(options.startupTimeoutMs, STARTUP_TIMEOUT_MS, "startupTimeoutMs");
|
|
63
|
+
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
64
|
+
const probeDeadline = Date.now() + timeoutMs;
|
|
65
|
+
for (;;) {
|
|
66
|
+
try {
|
|
67
|
+
const status = await call(home, "controller.status", {});
|
|
68
|
+
assertCompatibleControllerStatus(status, options.expectedIdentity);
|
|
69
|
+
return status;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (isUnavailable(error))
|
|
73
|
+
break;
|
|
74
|
+
// A closing endpoint may reset a read even after its owner exited.
|
|
75
|
+
// Retry only this observation: ambiguity never authorizes a spawn.
|
|
76
|
+
if (!(error instanceof ControllerClientError)
|
|
77
|
+
|| error.code !== "CONTROLLER_DELIVERY_UNKNOWN"
|
|
78
|
+
|| Date.now() >= probeDeadline)
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
await delay(pollMs);
|
|
68
82
|
}
|
|
69
83
|
// A handover may have started after the first lock check and before the
|
|
70
84
|
// failed discovery/socket call. Re-check before spawning so a managed
|
|
71
85
|
// Session can never resurrect the old Controller during an update.
|
|
72
86
|
await waitForForeignHandover(home, options);
|
|
73
|
-
const timeoutMs = positive(options.startupTimeoutMs, STARTUP_TIMEOUT_MS, "startupTimeoutMs");
|
|
74
|
-
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
75
87
|
const spawnController = options.spawnController ?? spawnDetachedFileTaskController;
|
|
76
88
|
// A readiness timeout is not exit evidence. Reuse the observable startup
|
|
77
89
|
// process for this exact Home rather than multiplying unready Controllers.
|
|
@@ -169,13 +181,27 @@ export async function stopFileTaskController(home, options = {}) {
|
|
|
169
181
|
await waitForForeignHandover(home, options);
|
|
170
182
|
const physical = options.call === undefined
|
|
171
183
|
? findLiveControllerProcessForHome(readHomeFilesystemId(home)) : undefined;
|
|
172
|
-
|
|
184
|
+
// A protected, Home-validated discovery can retain the generation fence
|
|
185
|
+
// even if the optional /proc ownership inventory is currently unreadable.
|
|
186
|
+
// It is not signal authority; physical recovery still revalidates ownership.
|
|
187
|
+
let captured = physical;
|
|
188
|
+
if (captured === undefined && options.call === undefined) {
|
|
189
|
+
try {
|
|
190
|
+
const discovery = await readControllerDiscovery(home);
|
|
191
|
+
captured = { pid: discovery.pid, processStartIdentity: discovery.processStartIdentity };
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (!isDefinitelyNotRunning(error))
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const expectedPid = options.expectedPid ?? captured?.pid;
|
|
173
199
|
const bounded = { ...options, requestTimeoutMs: options.requestTimeoutMs ?? 2000,
|
|
174
200
|
...(expectedPid === undefined ? {} : { expectedPid }) };
|
|
175
201
|
try {
|
|
176
202
|
const result = await stopControllerGracefully(home, bounded);
|
|
177
|
-
if (result.stopped &&
|
|
178
|
-
await waitForControllerProcessExit(
|
|
203
|
+
if (result.stopped && captured !== undefined) {
|
|
204
|
+
await waitForControllerProcessExit(captured, options);
|
|
179
205
|
}
|
|
180
206
|
if (result.stopped || options.call !== undefined)
|
|
181
207
|
return result;
|
|
@@ -185,16 +211,38 @@ export async function stopFileTaskController(home, options = {}) {
|
|
|
185
211
|
throw error;
|
|
186
212
|
}
|
|
187
213
|
const stopped = await stopOrphanedFileTaskController(home, options.shutdownTimeoutMs ?? CONTROLLER_SHUTDOWN_TIMEOUT_MS, { force: true, ...(expectedPid === undefined ? {} : { expectedPid }),
|
|
188
|
-
...(
|
|
214
|
+
...(captured === undefined ? {} : { expectedProcessStartIdentity: captured.processStartIdentity }) });
|
|
215
|
+
// No match is not exit proof: /proc ownership inspection may have become
|
|
216
|
+
// unreadable after we captured this exact generation. Physical recovery
|
|
217
|
+
// cannot authorize a successor until that original generation is gone.
|
|
218
|
+
if (captured !== undefined)
|
|
219
|
+
await waitForControllerProcessExit(captured, options);
|
|
189
220
|
return stopped === undefined ? { stopped: false, alreadyStopped: true } : { stopped: true, pid: stopped.pid };
|
|
190
221
|
}
|
|
191
|
-
async function waitForControllerProcessExit(
|
|
222
|
+
async function waitForControllerProcessExit(controller, options) {
|
|
192
223
|
const timeoutMs = positive(options.shutdownTimeoutMs, CONTROLLER_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
|
|
193
224
|
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
194
|
-
const homeFilesystemId = readHomeFilesystemId(home);
|
|
195
225
|
const deadline = Date.now() + timeoutMs;
|
|
196
|
-
|
|
226
|
+
for (;;) {
|
|
227
|
+
let observationError;
|
|
228
|
+
try {
|
|
229
|
+
if (!processGenerationIsLive(controller.pid, controller.processStartIdentity))
|
|
230
|
+
return;
|
|
231
|
+
if (process.platform === "linux") {
|
|
232
|
+
const observed = readLinuxProcessIdentity(controller.pid);
|
|
233
|
+
// An exact zombie has exited even before its parent reaps it.
|
|
234
|
+
if (observed?.startIdentity === controller.processStartIdentity && observed.state === "Z")
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
// OS identity can disappear before kill(pid, 0) reports absence.
|
|
240
|
+
// Keep observing within the existing stop budget, never infer exit.
|
|
241
|
+
observationError = error;
|
|
242
|
+
}
|
|
197
243
|
if (Date.now() >= deadline) {
|
|
244
|
+
if (observationError !== undefined)
|
|
245
|
+
throw observationError;
|
|
198
246
|
throw new ControllerClientError("CONTROLLER_TIMEOUT", `Controller process ${controller.pid} did not exit within ${timeoutMs} ms.`);
|
|
199
247
|
}
|
|
200
248
|
await delay(pollMs);
|
|
@@ -14,7 +14,7 @@ import { KeyedWorkQueue } from "../coordination/keyedWorkQueue.js";
|
|
|
14
14
|
import { MailboxScheduler } from "../coordination/mailboxScheduler.js";
|
|
15
15
|
import { nearestDeadlineBatch } from "../coordination/deadlineScheduler.js";
|
|
16
16
|
import { mailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
|
|
17
|
-
import { hasRuntimeCleanupObligation } from "../runtime/lifecycleReservation.js";
|
|
17
|
+
import { hasRuntimeCleanupObligation, runtimeCleanupDisposition } from "../runtime/lifecycleReservation.js";
|
|
18
18
|
import { formatTaskRecordReference } from "../task/taskRecordReference.js";
|
|
19
19
|
import { parseDurableJobAcknowledgeParams, parseDurableJobCancelParams, parseDurableJobRefParams, parseDurableJobStartParams } from "./jobControl.js";
|
|
20
20
|
const DEFAULT_RECONCILIATION_INTERVAL_MS = reconciliationIntervalMilliseconds();
|
|
@@ -211,6 +211,12 @@ async function processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost
|
|
|
211
211
|
const owner = runtimeOwner(target);
|
|
212
212
|
const batchId = runtimeLifecycleBatchIdentity(target, mailbox);
|
|
213
213
|
if (hasRuntimeCleanupObligation(mailbox)) {
|
|
214
|
+
// Upgrade is a graceful, exact-identity operation, never the destructive
|
|
215
|
+
// cleanup path. Its coordinator owns drain/restore; retain backpressure.
|
|
216
|
+
if (runtimeCleanupDisposition(mailbox) === "upgrade-host") {
|
|
217
|
+
markFailedRuntimeTarget(failedRoles, target);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
214
220
|
try {
|
|
215
221
|
if (lifecycleHost === undefined) {
|
|
216
222
|
throw new Error("Role runtime cleanup host is unavailable.");
|
|
@@ -698,6 +704,7 @@ export class FileTaskController {
|
|
|
698
704
|
#providerRetry;
|
|
699
705
|
#continuationReconciler;
|
|
700
706
|
#hostTerminalRecovery;
|
|
707
|
+
#lifecycleChecks;
|
|
701
708
|
#persistenceMetrics;
|
|
702
709
|
#diagnosticMetrics;
|
|
703
710
|
#current;
|
|
@@ -754,6 +761,7 @@ export class FileTaskController {
|
|
|
754
761
|
this.#schedules = options.schedules;
|
|
755
762
|
this.#continuationReconciler = options.continuationReconciler;
|
|
756
763
|
this.#hostTerminalRecovery = options.hostTerminalRecovery;
|
|
764
|
+
this.#lifecycleChecks = options.lifecycleChecks;
|
|
757
765
|
this.#globalInputDelivery = options.globalInputDelivery;
|
|
758
766
|
this.#messageHealth = options.messageHealth;
|
|
759
767
|
this.#providerRetry = options.providerRetry;
|
|
@@ -792,6 +800,7 @@ export class FileTaskController {
|
|
|
792
800
|
runtimeMetrics() {
|
|
793
801
|
const metrics = this.#lastRuntimeDrain?.metrics ?? ZERO_DRAIN_METRICS;
|
|
794
802
|
return {
|
|
803
|
+
sampler: this.#runtimeObserver?.metrics?.() ?? null,
|
|
795
804
|
messageHealth: this.#lastMessageHealth,
|
|
796
805
|
diagnostics: this.#diagnosticMetrics?.() ?? null,
|
|
797
806
|
phases: this.#phases.snapshot(),
|
|
@@ -841,6 +850,7 @@ export class FileTaskController {
|
|
|
841
850
|
return;
|
|
842
851
|
if (this.#stopped)
|
|
843
852
|
throw new Error("Controller runtime is stopped.");
|
|
853
|
+
this.#lifecycleChecks?.notify("startup");
|
|
844
854
|
this.#requestBackgroundPump();
|
|
845
855
|
this.#requestRuntimeObservation();
|
|
846
856
|
// Arm the Operator lane once for work that was already pending when the
|
|
@@ -861,6 +871,7 @@ export class FileTaskController {
|
|
|
861
871
|
}
|
|
862
872
|
stop() {
|
|
863
873
|
this.#stopped = true;
|
|
874
|
+
this.#lifecycleChecks?.close();
|
|
864
875
|
this.#workspaceWaitAbort.abort(new Error("Controller stopped while waiting for Project maintenance."));
|
|
865
876
|
this.#signalScheduler.stop();
|
|
866
877
|
this.#operatorSignalScheduler.stop();
|
|
@@ -904,6 +915,7 @@ export class FileTaskController {
|
|
|
904
915
|
if (this.#stopped)
|
|
905
916
|
throw new Error("Controller runtime is stopped.");
|
|
906
917
|
parseMailboxKey(key);
|
|
918
|
+
this.#lifecycleChecks?.notify("signal");
|
|
907
919
|
if (key === "operator") {
|
|
908
920
|
// Main lane owns durable Hook folding; the Operator lane is the sole
|
|
909
921
|
// consumer of the Operator delivery mailbox.
|
|
@@ -920,7 +932,7 @@ export class FileTaskController {
|
|
|
920
932
|
if (this.#stopped || this.#runtimeObserver === undefined
|
|
921
933
|
|| this.#runtimeObserverCurrent !== undefined)
|
|
922
934
|
return;
|
|
923
|
-
const running = this.#runtimeObserver.sample(this.#now()).then((keys) => {
|
|
935
|
+
const running = this.#phases.observe("runtime-sample", "home", () => this.#runtimeObserver.sample(this.#now(), this.#phases)).then((keys) => {
|
|
924
936
|
if (this.#stopped)
|
|
925
937
|
return;
|
|
926
938
|
for (const key of keys)
|
|
@@ -1016,6 +1028,8 @@ export class FileTaskController {
|
|
|
1016
1028
|
}
|
|
1017
1029
|
}
|
|
1018
1030
|
const firstRuntimeDrain = await this.#phases.observe("runtime-drain", "home", () => this.#drainRuntimeEvents());
|
|
1031
|
+
if (scope.kind === "full")
|
|
1032
|
+
this.#lifecycleChecks?.notify("periodic");
|
|
1019
1033
|
if (scope.kind === "full" && this.#hostTerminalRecovery !== undefined) {
|
|
1020
1034
|
await this.#observeMaintenance("Host terminal recovery", this.#hostTerminalRecovery);
|
|
1021
1035
|
}
|
|
@@ -1257,6 +1271,8 @@ export class FileTaskController {
|
|
|
1257
1271
|
? await processor.drainAsync(this.#now())
|
|
1258
1272
|
: processor.drain(this.#now());
|
|
1259
1273
|
this.#lastRuntimeDrain = result;
|
|
1274
|
+
if (result.acknowledgedEventIds.length > 0)
|
|
1275
|
+
this.#lifecycleChecks?.notify("events-persisted");
|
|
1260
1276
|
this.#runtimeDrainPasses += 1;
|
|
1261
1277
|
const metrics = result.metrics ?? ZERO_DRAIN_METRICS;
|
|
1262
1278
|
this.#runtimeListedEvents += metrics.listedEventCount;
|
|
@@ -1779,6 +1795,7 @@ export async function startFileTaskController(home, store, delivery, dispatcher,
|
|
|
1779
1795
|
await runtime.shutdownAndDrain();
|
|
1780
1796
|
}, {
|
|
1781
1797
|
domainIdentity: options.domainIdentity,
|
|
1798
|
+
onRequestDiagnostic: options.onRequestDiagnostic,
|
|
1782
1799
|
status: ({ commandObserver, eventLoopDelay }) => ({
|
|
1783
1800
|
...runtime.runtimeMetrics(),
|
|
1784
1801
|
commands: {
|
|
@@ -13,13 +13,15 @@ const NUMERIC_KEYS = new Set([
|
|
|
13
13
|
"completed", "failed", "received", "inFlight", "maximumMs", "maximumLagMs",
|
|
14
14
|
"maximumObservedAtMs", "oldestInFlightAgeMs", "startedAtMs", "elapsedMs",
|
|
15
15
|
"ageMs", "depth", "semanticDepth", "progressDepth", "pendingKeys", "queueDepth",
|
|
16
|
-
"hits", "rebuilds", "invalidations", "passes", "listedEvents", "selectedEvents"
|
|
16
|
+
"hits", "rebuilds", "invalidations", "passes", "listedEvents", "selectedEvents",
|
|
17
|
+
"acceptedAtMs", "receivedAtMs", "routedAtMs", "handlerCompletedAtMs", "flushCompletedAtMs",
|
|
18
|
+
"handlerFailed", "flushFailed", "samples", "sources", "published", "usagePublications"
|
|
17
19
|
]);
|
|
18
20
|
const CONTAINER_KEYS = new Set([
|
|
19
21
|
"commands", "observations", "routes", "eventLoopDelay", "phases", "reads",
|
|
20
22
|
"persistence", "main", "worker", "database", "roundTrips", "work", "body",
|
|
21
23
|
"lock", "commit", "payloadReads", "inbox", "scheduler", "projectionCache",
|
|
22
|
-
"active", "recent", "slowest", "latest"
|
|
24
|
+
"active", "recent", "slowest", "latest", "transport", "sampler"
|
|
23
25
|
]);
|
|
24
26
|
const ERROR_CODES = new Set([
|
|
25
27
|
"SQLITE_BUSY", "SQLITE_LOCKED", "SQLITE_IOERR", "SQLITE_FULL", "SQLITE_READONLY",
|
|
@@ -101,6 +103,12 @@ export class ControllerDiagnosticJournal {
|
|
|
101
103
|
error(error) {
|
|
102
104
|
this.#enqueue("error", { error: errorEvidence(error) });
|
|
103
105
|
}
|
|
106
|
+
/** Completion-driven evidence bypasses the periodic recent-array/sample
|
|
107
|
+
* window, but uses the same allowlist, queue/record/file bounds and async I/O.
|
|
108
|
+
* Disk failure, queue pressure and force-kill remain explicit evidence gaps. */
|
|
109
|
+
request(record) {
|
|
110
|
+
this.#enqueue("request", { request: safeSnapshot(record) });
|
|
111
|
+
}
|
|
104
112
|
sample(snapshot, lagMs) {
|
|
105
113
|
const now = monotonicMilliseconds();
|
|
106
114
|
if (now - this.#lastSampleAt < 1000) {
|