@zq-silk/yui 2.22.0 → 2.25.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 +9 -4
- package/dist/artifacts/artifactCapability.js +6 -6
- package/dist/artifacts/gitArtifactRef.js +10 -10
- package/dist/artifacts/jobArtifact.js +5 -5
- package/dist/artifacts/managedGit.js +15 -12
- package/dist/artifacts/taskArtifactRepository.js +35 -33
- package/dist/cli/commandCatalog.js +2 -2
- package/dist/cli/updateCommand.js +20 -3
- package/dist/cli/updateOrchestrator.js +13 -0
- package/dist/cli/updatePorts.js +27 -0
- package/dist/cli.js +9 -2
- package/dist/commands/configCommands.js +64 -1
- package/dist/commands/projectCommands.js +9 -9
- package/dist/commands/taskCompletionGate.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +1 -1
- package/dist/commands/taskPublicationAdoptCommand.js +1 -1
- package/dist/commands/taskRoleRuntimeStatus.js +4 -0
- package/dist/commands/taskUpstreamCommands.js +1 -1
- package/dist/config/configCatalog.js +3 -0
- package/dist/config/gitConfig.js +20 -0
- package/dist/config/yuiConfig.js +1 -0
- package/dist/context/currentSkills.js +32 -1
- package/dist/context/sessionBootstrapManifest.js +17 -4
- package/dist/context/sessionSkillNotice.js +33 -12
- package/dist/controlPlaneCli.js +4 -4
- package/dist/controller/clientRuntime.js +37 -18
- package/dist/controller/controller.js +16 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +2 -0
- package/dist/controller/hostLifecycle.js +153 -0
- package/dist/controller/hostLifecycleChecks.js +27 -0
- package/dist/controller/hostUpgrade.js +344 -0
- package/dist/controller/jobArtifactControl.js +3 -3
- package/dist/controller/jobDefinitionControl.js +1 -1
- package/dist/controller/runtime.js +67 -16
- package/dist/controller/sessionNotify.js +5 -46
- package/dist/execution/codexThreadNaming.js +54 -43
- package/dist/executor/fileRoleLaunchPlanner.js +18 -3
- package/dist/integration/gitIntegrationService.js +1 -1
- package/dist/kernel/builtinCapabilities.js +3 -3
- package/dist/repository/gitCommand.js +153 -0
- package/dist/repository/gitCompatibility.js +3 -2
- package/dist/repository/gitWorkspace.js +579 -563
- package/dist/repository/gitWorktreeInventory.js +15 -19
- package/dist/repository/projectRefresh.js +1 -1
- package/dist/repository/taskBaseFreshness.js +1 -4
- package/dist/repository/taskWorkspacePreparer.js +1 -1
- package/dist/runtime/agentEndpoint.js +11 -1
- package/dist/runtime/agentEndpointIdentity.js +2 -0
- package/dist/runtime/agentHost.js +167 -42
- package/dist/runtime/agentHostImplementation.js +3 -1
- package/dist/runtime/codexAppServerRuntime.js +3 -0
- package/dist/runtime/codexInteractiveHost.js +62 -17
- 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/nativeExecutable.js +5 -2
- package/dist/runtime/nativeTitleReceipt.js +31 -0
- package/dist/runtime/nativeTitleSync.js +192 -0
- package/dist/runtime/platform.js +11 -0
- package/dist/runtime/providerRetry.js +4 -4
- package/dist/runtime/structuredProviderHost.js +42 -10
- package/dist/storage/sqliteSchema.js +30 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskStore.js +7 -1
- package/dist/web/webConversation.js +2 -2
- package/dist/web/webMaterials.js +19 -19
- package/dist/web/webTaskSurface.js +3 -3
- package/dist/workspace/workItemChangeSetManager.js +3 -3
- package/docs/managed-turn-and-session-runtime.md +96 -1
- package/docs/managed-turn-and-session-runtime.zh-CN.md +70 -1
- package/docs/project-refresh.md +3 -2
- package/docs/project-refresh.zh-CN.md +2 -1
- package/docs/release-workflow.md +86 -2
- package/docs/release-workflow.zh-CN.md +65 -1
- package/docs/roles-and-configuration.md +34 -0
- package/docs/roles-and-configuration.zh-CN.md +27 -0
- package/docs/storage-baseline.md +14 -1
- package/docs/storage-baseline.zh-CN.md +7 -1
- package/docs/testing/verification-levels.md +7 -7
- package/docs/testing/verification-levels.zh-CN.md +6 -6
- package/i18n/README.zh-CN.md +8 -4
- package/package.json +1 -1
- package/skills/yui-runtime/SKILL.md +7 -2
- package/skills/yui-runtime/VERSION +1 -1
- package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
- package/dist/runtime/native/darwin-x64/process-identity +0 -0
|
@@ -109,7 +109,7 @@ async function diagnoseProject(args, store, options) {
|
|
|
109
109
|
data: { project, diagnosis: "no-remote" }
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
-
const git = options.git ?? new NodeGitWorkspace();
|
|
112
|
+
const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
|
|
113
113
|
const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
|
|
114
114
|
try {
|
|
115
115
|
requireUnchangedMaintenanceProject(store, project, "diagnose");
|
|
@@ -153,7 +153,7 @@ async function discoverProjects(args, store, options) {
|
|
|
153
153
|
const workspace = store.getConfig().defaultWorkspace;
|
|
154
154
|
if (workspace === undefined)
|
|
155
155
|
throw usageError("No default workspace is configured; run yui setup.");
|
|
156
|
-
const git = options.git ?? new NodeGitWorkspace();
|
|
156
|
+
const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
|
|
157
157
|
const entries = await readdir(workspace, { withFileTypes: true });
|
|
158
158
|
const discovered = [];
|
|
159
159
|
for (const entry of entries) {
|
|
@@ -195,7 +195,7 @@ function renderDiscoveredProjects(projects) {
|
|
|
195
195
|
async function cloneProject(args, store, options) {
|
|
196
196
|
const usage = "Project clone usage: yui project clone <name> <remote> [--alias <name> ...] [--stable <ref>] [--development <ref>] [--external].";
|
|
197
197
|
const parsed = parseCloneArguments(args, usage);
|
|
198
|
-
const git = options.git ?? new NodeGitWorkspace();
|
|
198
|
+
const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
|
|
199
199
|
const name = validateProjectName(parsed.name);
|
|
200
200
|
const stableRef = parsed.stable ?? "HEAD";
|
|
201
201
|
const developmentRef = parsed.development ?? stableRef;
|
|
@@ -282,7 +282,7 @@ async function migrateProject(args, store, options) {
|
|
|
282
282
|
if (project.remoteUrl === undefined) {
|
|
283
283
|
throw usageError(`Project migrate requires a remote URL: ${project.id}.`);
|
|
284
284
|
}
|
|
285
|
-
const git = options.git ?? new NodeGitWorkspace();
|
|
285
|
+
const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
|
|
286
286
|
const destination = managedProjectPath(store.rootDirectory(), project.id);
|
|
287
287
|
// Migration rewrites the Project's Git repository: hold the per-Project
|
|
288
288
|
// maintenance fence so no rebuild/archive/cleanup (or a second migrate)
|
|
@@ -444,7 +444,7 @@ async function addProject(args, store, options) {
|
|
|
444
444
|
const usage = "Project add usage: yui project add <name> <path> [--alias <name> ...] [--remote <url>] [--stable <ref>] [--development <ref>].";
|
|
445
445
|
const parsed = parseAddArguments(args, usage);
|
|
446
446
|
assertOutsideManagedWorktrees(parsed.path, store.rootDirectory());
|
|
447
|
-
const git = options.git ?? new NodeGitWorkspace();
|
|
447
|
+
const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
|
|
448
448
|
const head = await git.inspect(parsed.path, "HEAD");
|
|
449
449
|
assertOutsideManagedWorktrees(head.root, store.rootDirectory());
|
|
450
450
|
if (!await git.isClean(head.root)) {
|
|
@@ -483,7 +483,7 @@ async function updateProject(args, store, options) {
|
|
|
483
483
|
: { developmentBranch: parsed.development })
|
|
484
484
|
};
|
|
485
485
|
assertProjectAvailable(store, updateProjectMetadata(current, patch, now), current.id);
|
|
486
|
-
const git = options.git ?? new NodeGitWorkspace();
|
|
486
|
+
const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
|
|
487
487
|
if (parsed.stable !== undefined) {
|
|
488
488
|
const head = await git.inspect(current.path, "HEAD");
|
|
489
489
|
if (!await git.isClean(head.root)) {
|
|
@@ -568,7 +568,7 @@ async function resetProject(args, store, options) {
|
|
|
568
568
|
if (project.stableBranch !== project.developmentBranch) {
|
|
569
569
|
throw usageError(`Project reset requires matching stable and development branches: ${project.id}.`);
|
|
570
570
|
}
|
|
571
|
-
const git = options.git ?? new NodeGitWorkspace();
|
|
571
|
+
const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
|
|
572
572
|
const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
|
|
573
573
|
try {
|
|
574
574
|
// Re-read under the fence so a concurrent catalog change can never drive
|
|
@@ -693,7 +693,7 @@ async function replaceProject(args, store, options) {
|
|
|
693
693
|
throw usageError(`Project replace discards the current checkout and re-clones from ${project.remoteUrl}. `
|
|
694
694
|
+ "Re-run with --discard-local to acknowledge that the checkout and any uncommitted state will be discarded.");
|
|
695
695
|
}
|
|
696
|
-
const git = options.git ?? new NodeGitWorkspace();
|
|
696
|
+
const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
|
|
697
697
|
const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
|
|
698
698
|
try {
|
|
699
699
|
const current = requireProject(store, project.id);
|
|
@@ -909,7 +909,7 @@ async function deleteProjectCommand(args, store, options) {
|
|
|
909
909
|
// Heal a crashed earlier attempt before the prechecks turn.
|
|
910
910
|
await healCheckoutSwap({ currentPath: project.path, backupPath: tombstone });
|
|
911
911
|
if (existsSync(project.path)) {
|
|
912
|
-
const git = options.git ?? new NodeGitWorkspace();
|
|
912
|
+
const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
|
|
913
913
|
const worktrees = await git.listWorktrees(project.path);
|
|
914
914
|
const linked = worktrees.filter((path) => path !== project.path);
|
|
915
915
|
if (linked.length > 0) {
|
|
@@ -27,7 +27,7 @@ export async function verifyTaskCompletionPublishedTree(taskId, publicationId, s
|
|
|
27
27
|
throw usageError(`Publication ${publication.id} must record exact local and remote commits.`);
|
|
28
28
|
}
|
|
29
29
|
const workspace = requireTaskWorkspace(store, task);
|
|
30
|
-
const git = options.git ?? new NodeGitWorkspace();
|
|
30
|
+
const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
|
|
31
31
|
const actualHeads = new Map();
|
|
32
32
|
for (const taskBinding of task.projectBindings) {
|
|
33
33
|
const entry = workspaceProjectEntry(workspace, taskBinding.projectId);
|
|
@@ -123,7 +123,7 @@ async function start(args, store, home, now, options) {
|
|
|
123
123
|
throw usageError(`Task main worktree is not ready; reconcile the Task first: ${task.id}.`);
|
|
124
124
|
}
|
|
125
125
|
taskIntegrationTargetCheckout(store, options.environment, task.id, project.id, targetRef);
|
|
126
|
-
const expectedHead = (await new NodeGitWorkspace().inspect(mainEntry.path, targetRef)).baseCommit;
|
|
126
|
+
const expectedHead = (await new NodeGitWorkspace(() => store.getConfig()).inspect(mainEntry.path, targetRef)).baseCommit;
|
|
127
127
|
const snapshotEntry = candidate.workspace.entries.find(({ projectId }) => projectId === project.id);
|
|
128
128
|
const resultCommit = candidate.gitSnapshot.projects.find(({ projectId }) => projectId === project.id)?.commit;
|
|
129
129
|
if (snapshotEntry === undefined || resultCommit === undefined) {
|
|
@@ -74,7 +74,7 @@ export async function runTaskPublicationAdoptCommand(args, store, options = {})
|
|
|
74
74
|
return { task, publication, completion, acceptedCommit, workspace, entry, integration, actor };
|
|
75
75
|
};
|
|
76
76
|
const facts = store.transaction(read);
|
|
77
|
-
const git = options.git ?? new NodeGitWorkspace();
|
|
77
|
+
const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
|
|
78
78
|
const localCommit = facts.publication.localCommit;
|
|
79
79
|
const [acceptedTree, candidateTree, diff] = await Promise.all([
|
|
80
80
|
git.resolveTree(facts.entry.path, facts.acceptedCommit),
|
|
@@ -19,6 +19,10 @@ export function taskRoleHostDiagnostic(host) {
|
|
|
19
19
|
const delivery = snapshot.eventDelivery;
|
|
20
20
|
return [
|
|
21
21
|
`Provider Host=${snapshot.state}`,
|
|
22
|
+
`loaded Host=${snapshot.hostImplementation?.generation ?? "unreported"}`,
|
|
23
|
+
`loaded Endpoint=${snapshot.endpointImplementation?.generation ?? "unreported"}`,
|
|
24
|
+
snapshot.hostUpgrade === undefined ? undefined
|
|
25
|
+
: `Host upgrade=${snapshot.hostUpgrade.state} (${snapshot.hostUpgrade.requestId})${snapshot.hostUpgrade.detail ? `: ${snapshot.hostUpgrade.detail}` : ""}`,
|
|
22
26
|
`terminal reconciliation=${snapshot.controlCapabilities?.includes("reconcile-turn") ? "supported" : "unreported/unsupported"}`,
|
|
23
27
|
delivery === undefined ? "event delivery=unreported" :
|
|
24
28
|
`pending facts=${delivery.pending}; native terminals awaiting acknowledgement=${delivery.pendingTerminals}`,
|
|
@@ -48,7 +48,7 @@ async function integrateUpstream(args, store, home, options) {
|
|
|
48
48
|
throw usageError(`Task must be active to integrate upstream: ${taskId}/${task.status}.`);
|
|
49
49
|
}
|
|
50
50
|
taskLocalActor(store, options.environment, task.id);
|
|
51
|
-
const git = options.git ?? new NodeGitWorkspace();
|
|
51
|
+
const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
|
|
52
52
|
const now = options.now ?? (() => new Date());
|
|
53
53
|
await new FileTaskWorkspacePreparer(home, store, git, now)
|
|
54
54
|
.prepareTaskWorkspace(task.id);
|
|
@@ -20,6 +20,9 @@ export const CONFIG_DEFINITIONS = Object.freeze([
|
|
|
20
20
|
{ key: "resources-quarantine-ttl-hours", domain: "resources", property: "resourcesQuarantineTtlHours", label: "Resource quarantine TTL", summary: "Default observation window before purge, 1-720 hours (default: 24).", takesEffect: "The next resource GC command without an explicit TTL override." },
|
|
21
21
|
{ key: "tmux-bin", domain: "tools", property: "tmuxBin", label: "Tmux bin", summary: "Command or path to the tmux binary (default: tmux).", takesEffect: "The next CLI-owned tmux invocation; restart the Controller for Controller-owned launches." },
|
|
22
22
|
{ key: "tmux-history-limit", domain: "tools", property: "tmuxHistoryLimit", label: "Tmux history limit", summary: "History lines retained for newly created tmux sessions, 1000-1000000 (default: 100000).", takesEffect: "New tmux sessions; existing sessions report drift until recreated." },
|
|
23
|
+
{ key: "git-local-timeout-seconds", domain: "tools", property: "gitLocalTimeoutSeconds", label: "Git local total timeout", summary: "Total elapsed-time limit for each local Git command, 1-86400 seconds (default: 30). Independent of remote transport budgets.", takesEffect: "The next local Git command, including Controller-owned commands; in-flight commands keep their budgets." },
|
|
24
|
+
{ key: "git-remote-inactivity-timeout-seconds", domain: "tools", property: "gitRemoteInactivityTimeoutSeconds", label: "Git remote inactivity timeout", summary: "Maximum silence on stdout/stderr for clone, remote fetch and ls-remote, 1-86400 seconds (default: 120). Local Git uses its separate configurable total budget.", takesEffect: "The next remote Git command, including Controller-owned commands; in-flight commands keep their budgets." },
|
|
25
|
+
{ key: "git-remote-timeout-seconds", domain: "tools", property: "gitRemoteTimeoutSeconds", label: "Git remote total timeout", summary: "Total elapsed-time limit for each clone, remote fetch or ls-remote, 1-86400 seconds (default: 1800), even while progress continues.", takesEffect: "The next remote Git command, including Controller-owned commands; in-flight commands keep their budgets." },
|
|
23
26
|
{ key: "telemetry-enabled", domain: "tools", property: "telemetryEnabled", label: "Diagnostic telemetry", summary: "Enable the optional SQLite diagnostic telemetry projection (default: false); requires a SQLite Home.", takesEffect: "Standalone telemetry commands immediately; scheduler writes after Controller restart." },
|
|
24
27
|
{ key: "telemetry-terminal-keep", domain: "tools", property: "telemetryTerminalKeep", label: "Telemetry terminal keep", summary: "Positive-integer rows retained per terminal AgentRun (default: 200).", takesEffect: "Explicit retention immediately; automatic retention after the Controller restarts." },
|
|
25
28
|
{ key: "telemetry-run-cap", domain: "tools", property: "telemetryRunCap", label: "Telemetry AgentRun cap", summary: `Positive-integer maximum rows retained per AgentRun, at most ${MAX_RUN_CAP} (default: 50000).`, takesEffect: "Explicit retention immediately; automatic write-time capping after the Controller restarts." }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS = 30;
|
|
2
|
+
export const DEFAULT_GIT_REMOTE_INACTIVITY_TIMEOUT_SECONDS = 120;
|
|
3
|
+
export const DEFAULT_GIT_REMOTE_TIMEOUT_SECONDS = 1_800;
|
|
4
|
+
export function resolveGitLocalTimeoutSeconds(value) {
|
|
5
|
+
return timeoutSeconds(value, DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS, "gitLocalTimeoutSeconds");
|
|
6
|
+
}
|
|
7
|
+
export function resolveGitRemoteInactivityTimeoutSeconds(value) {
|
|
8
|
+
return timeoutSeconds(value, DEFAULT_GIT_REMOTE_INACTIVITY_TIMEOUT_SECONDS, "gitRemoteInactivityTimeoutSeconds");
|
|
9
|
+
}
|
|
10
|
+
export function resolveGitRemoteTimeoutSeconds(value) {
|
|
11
|
+
return timeoutSeconds(value, DEFAULT_GIT_REMOTE_TIMEOUT_SECONDS, "gitRemoteTimeoutSeconds");
|
|
12
|
+
}
|
|
13
|
+
function timeoutSeconds(value, fallback, label) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return fallback;
|
|
16
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 86_400) {
|
|
17
|
+
throw new TypeError(`${label} must be an integer from 1 to 86400.`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
package/dist/config/yuiConfig.js
CHANGED
|
@@ -91,6 +91,7 @@ export const MAX_CONTROLLER_TASK_CONCURRENCY = 32;
|
|
|
91
91
|
export const DEFAULT_AGENT_LAUNCH_INACTIVITY_TIMEOUT_SECONDS = 300;
|
|
92
92
|
export const DEFAULT_DELIVERY_TIMEOUT_SECONDS = 120;
|
|
93
93
|
export const DEFAULT_TMUX_HISTORY_LIMIT = 100_000;
|
|
94
|
+
export { DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS, DEFAULT_GIT_REMOTE_INACTIVITY_TIMEOUT_SECONDS, DEFAULT_GIT_REMOTE_TIMEOUT_SECONDS, resolveGitLocalTimeoutSeconds, resolveGitRemoteInactivityTimeoutSeconds, resolveGitRemoteTimeoutSeconds } from "./gitConfig.js";
|
|
94
95
|
export function resolveRuntimeHealth(configured) {
|
|
95
96
|
if (configured === undefined || configured === null)
|
|
96
97
|
return DEFAULT_RUNTIME_HEALTH_POLICY;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { accessSync, constants, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { access, lstat, readFile, realpath } from "node:fs/promises";
|
|
2
3
|
import { join, resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
export const BUILTIN_YUI_SKILLS = new Set([
|
|
@@ -41,9 +42,39 @@ export function checkCurrentSkills(home, ids) {
|
|
|
41
42
|
}
|
|
42
43
|
});
|
|
43
44
|
}
|
|
45
|
+
/** Advisory lifecycle observation uses asynchronous filesystem I/O; it neither
|
|
46
|
+
* blocks a state transition nor substitutes for the exact per-input check. */
|
|
47
|
+
export async function checkCurrentSkillsAsync(home, ids, signal) {
|
|
48
|
+
const skills = [];
|
|
49
|
+
for (const id of new Set(ids)) {
|
|
50
|
+
signal.throwIfAborted();
|
|
51
|
+
const path = currentSkillPath(home, id);
|
|
52
|
+
const versionPath = join(path, "VERSION");
|
|
53
|
+
try {
|
|
54
|
+
for (const file of [join(path, "SKILL.md"), versionPath]) {
|
|
55
|
+
if (await realpath(file) !== resolve(file) || !(await lstat(file)).isFile()) {
|
|
56
|
+
throw new Error(`Skill files must be regular files without symlinks: ${file}.`);
|
|
57
|
+
}
|
|
58
|
+
await access(file, constants.R_OK);
|
|
59
|
+
}
|
|
60
|
+
if ((await lstat(versionPath)).size > 128)
|
|
61
|
+
throw new Error("VERSION exceeds 128 bytes.");
|
|
62
|
+
const version = (await readFile(versionPath, { encoding: "utf8", signal })).trim();
|
|
63
|
+
if (!/^[1-9][0-9]{0,8}$/u.test(version))
|
|
64
|
+
throw new Error("VERSION must contain a positive internal integer.");
|
|
65
|
+
skills.push({ id, path, version, versionPath });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`Current Skill ${id} is missing, unreadable or invalid at ${path}. `
|
|
69
|
+
+ `${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return skills;
|
|
73
|
+
}
|
|
44
74
|
export const SKILL_CHECK_COMMAND = "yui session skills --json";
|
|
45
75
|
export const SKILL_REUSE_RULE = "The managed Host checks current Skill versions/availability before each input; "
|
|
46
|
-
+ "
|
|
76
|
+
+ "unchanged inputs carry no notice or marker, and do not require a duplicate check. "
|
|
77
|
+
+ "For unmediated clients or uncertain current input routing, check through the Manifest's command; conversation reuse alone does not establish Host mediation. Keep routine checks internal, not user-facing progress. "
|
|
47
78
|
+ "Reuse unchanged full instructions in context; messages alone require no reload. "
|
|
48
79
|
+ "Read affected entries/references fully on change or localized loss. On first entry, recovery or lost context, "
|
|
49
80
|
+ "rebuild the current working set from applicable Skill entry points and action-relevant references, not all past procedures. "
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { chmodSync, readFileSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
4
5
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
5
6
|
import { validateSkillPackageRef } from "./skillPackage.js";
|
|
6
|
-
import { checkCurrentSkills, SKILL_CHECK_COMMAND, SKILL_REUSE_RULE } from "./currentSkills.js";
|
|
7
|
+
import { checkCurrentSkills, checkCurrentSkillsAsync, SKILL_CHECK_COMMAND, SKILL_REUSE_RULE } from "./currentSkills.js";
|
|
7
8
|
import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
|
|
8
9
|
export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
|
|
9
10
|
/** Portable read pointers accompany every managed input, including the first
|
|
@@ -22,11 +23,11 @@ export function withSessionContextPointer(text, environment) {
|
|
|
22
23
|
* Unmediated/native clients retain the explicit diagnostic fallback. */
|
|
23
24
|
export function sessionContextInstructions(manifestPath, digest) {
|
|
24
25
|
return `Yui Session Manifest: ${manifestPath}${digest === undefined ? "" : ` (digest ${digest})`}. `
|
|
25
|
-
+ "
|
|
26
|
-
+ `
|
|
26
|
+
+ "The managed Host checks Skills internally and injects guidance only when loading is needed; absence of a notice does not require another check. "
|
|
27
|
+
+ `For unmediated clients or uncertain current input routing, run its contextProtocol.skillCheckCommand (fallback: ${SKILL_CHECK_COMMAND}); conversation reuse alone does not establish Host mediation. `
|
|
27
28
|
+ "On first entry or lost instructions, read the current applicable Skill entry points, then only references needed for the current action. "
|
|
28
29
|
+ "On change reload affected instructions. Compaction does not require restoring every previously used procedure. "
|
|
29
|
-
+ "Preserve the Manifest Role profile; metadata never proves loading or memory.";
|
|
30
|
+
+ "Preserve the Manifest Role profile; metadata never proves loading or memory. Keep routine role, version and empty-inbox checks internal.";
|
|
30
31
|
}
|
|
31
32
|
/** The caller must already pass the ordinary managed Session/CLI/Home checks.
|
|
32
33
|
* Old manifests remain immutable audit records, while this read resolves their
|
|
@@ -40,6 +41,15 @@ export function checkSessionSkills(home, manifestPath) {
|
|
|
40
41
|
historicalPackagesAreAuditOnly: true
|
|
41
42
|
};
|
|
42
43
|
}
|
|
44
|
+
export async function checkSessionSkillsAsync(home, manifestPath, signal) {
|
|
45
|
+
const manifest = validateSessionBootstrapManifest(JSON.parse(await readFile(resolve(manifestPath), { encoding: "utf8", signal })));
|
|
46
|
+
return {
|
|
47
|
+
skills: await checkCurrentSkillsAsync(home, manifest.skills.map(skill => skill.id), signal),
|
|
48
|
+
roleProfileRef: manifest.roleProfileRef,
|
|
49
|
+
rule: SKILL_REUSE_RULE,
|
|
50
|
+
historicalPackagesAreAuditOnly: true
|
|
51
|
+
};
|
|
52
|
+
}
|
|
43
53
|
/** Read back one immutable Session Manifest and verify its content digest. */
|
|
44
54
|
export function readSessionBootstrapManifest(path) {
|
|
45
55
|
const source = resolve(path);
|
|
@@ -50,6 +60,9 @@ export function readSessionBootstrapManifest(path) {
|
|
|
50
60
|
catch (error) {
|
|
51
61
|
throw new Error(`Session Manifest is unreadable: ${source}.`, { cause: error });
|
|
52
62
|
}
|
|
63
|
+
return validateSessionBootstrapManifest(parsed);
|
|
64
|
+
}
|
|
65
|
+
function validateSessionBootstrapManifest(parsed) {
|
|
53
66
|
if (parsed === null || typeof parsed !== "object") {
|
|
54
67
|
throw new Error("Session Manifest is invalid.");
|
|
55
68
|
}
|
|
@@ -1,4 +1,26 @@
|
|
|
1
|
-
import { checkSessionSkills } from "./sessionBootstrapManifest.js";
|
|
1
|
+
import { checkSessionSkills, checkSessionSkillsAsync } from "./sessionBootstrapManifest.js";
|
|
2
|
+
/** Early advisory checks do not commit notices or assert model memory. The
|
|
3
|
+
* next input still validates its exact metadata and emits only its own delta.
|
|
4
|
+
* Invalid metadata may reject that input, never a Session state transition. */
|
|
5
|
+
export function createSessionSkillLifecycleCheck(home, environment) {
|
|
6
|
+
return {
|
|
7
|
+
id: "session-skills",
|
|
8
|
+
run: async (transition, signal) => {
|
|
9
|
+
if (!["connected", "interactive-connected", "settled"].includes(transition.event.type))
|
|
10
|
+
return;
|
|
11
|
+
const manifest = environment().YUI_SESSION_MANIFEST;
|
|
12
|
+
if (!manifest || signal.aborted)
|
|
13
|
+
return;
|
|
14
|
+
try {
|
|
15
|
+
await checkSessionSkillsAsync(home, manifest, signal);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (!signal.aborted)
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
2
24
|
/** Host-local last accepted metadata, never a model loading/memory receipt.
|
|
3
25
|
* A new Host/attachment starts cold. No durable schema or input replay is added. */
|
|
4
26
|
export function createSessionSkillNotice(home) {
|
|
@@ -10,9 +32,7 @@ export function createSessionSkillNotice(home) {
|
|
|
10
32
|
prepare(text, environment, sessionKey, options = {}) {
|
|
11
33
|
const manifest = environment.YUI_SESSION_MANIFEST;
|
|
12
34
|
if (manifest === undefined)
|
|
13
|
-
return { text, commit() { } };
|
|
14
|
-
if (options.recovery)
|
|
15
|
-
invalidate();
|
|
35
|
+
return { text, inputText: text, commit() { } };
|
|
16
36
|
let current;
|
|
17
37
|
try {
|
|
18
38
|
current = checkSessionSkills(home, manifest);
|
|
@@ -21,7 +41,7 @@ export function createSessionSkillNotice(home) {
|
|
|
21
41
|
invalidate();
|
|
22
42
|
throw error; // Do not submit business input with an invalid current installation.
|
|
23
43
|
}
|
|
24
|
-
const key = JSON.stringify([sessionKey, manifest, current.roleProfileRef]);
|
|
44
|
+
const key = JSON.stringify([sessionKey, manifest, current.roleProfileRef, options.contextEpoch]);
|
|
25
45
|
const skills = new Map(current.skills.map(skill => [skill.id, JSON.stringify({
|
|
26
46
|
id: skill.id, path: skill.path, version: skill.version
|
|
27
47
|
})]));
|
|
@@ -30,17 +50,18 @@ export function createSessionSkillNotice(home) {
|
|
|
30
50
|
.map(([, value]) => value);
|
|
31
51
|
const removed = !cold && [...previous.skills.keys()].some(id => !skills.has(id));
|
|
32
52
|
// Every input is checked, but only a lifecycle/metadata event needs an
|
|
33
|
-
// instruction-loading notice.
|
|
34
|
-
|
|
35
|
-
const lines = ["Yui Host checked current Skills; no Agent check needed."];
|
|
53
|
+
// instruction-loading notice. Unchanged input is passed through exactly.
|
|
54
|
+
const lines = [];
|
|
36
55
|
if (cold || changed.length > 0 || removed) {
|
|
37
|
-
lines.push(`Yui Session Manifest: ${manifest}`, cold
|
|
38
|
-
? `
|
|
39
|
-
: "Skill metadata changed: load affected entries and applicable references; retain other complete instructions.", `Current Skill pointers: [${(removed ? [...skills.values()] : changed).join(",")}]`, "
|
|
56
|
+
lines.push(`Yui Session Manifest: ${manifest}`, "Host-managed Skill checks are active; unchanged input has no notice. Detached/unmediated clients must use the Manifest check fallback.", cold
|
|
57
|
+
? `Read the exact Role profile at ${current.roleProfileRef.path} (not a Task projection) and all current Skill entry points below. Follow references only for the current action.`
|
|
58
|
+
: "Skill metadata changed: load affected entries and applicable references; retain other complete instructions.", `Current Skill pointers: [${(removed ? [...skills.values()] : changed).join(",")}]`, "Read required files through EOF. Metadata is not loaded instructions. Keep routine checks internal.");
|
|
40
59
|
}
|
|
41
60
|
const preparedGeneration = generation;
|
|
42
61
|
return {
|
|
43
|
-
text: `${lines.join("\n")}\n\n${text}`,
|
|
62
|
+
text: lines.length === 0 ? text : `${lines.join("\n")}\n\n${text}`,
|
|
63
|
+
inputText: text,
|
|
64
|
+
...(lines.length === 0 ? {} : { context: lines.join("\n") }),
|
|
44
65
|
commit() {
|
|
45
66
|
if (generation !== preparedGeneration)
|
|
46
67
|
return;
|
package/dist/controlPlaneCli.js
CHANGED
|
@@ -1111,7 +1111,7 @@ export async function main() {
|
|
|
1111
1111
|
if (action === "list") {
|
|
1112
1112
|
if (resolved.length !== 4)
|
|
1113
1113
|
throw usageError(usage);
|
|
1114
|
-
data = await listArtifactsCapability(home, taskId);
|
|
1114
|
+
data = await listArtifactsCapability(home, taskId, () => store.getConfig());
|
|
1115
1115
|
}
|
|
1116
1116
|
else if (action === "read") {
|
|
1117
1117
|
const relativePath = resolved[4];
|
|
@@ -1120,7 +1120,7 @@ export async function main() {
|
|
|
1120
1120
|
throw usageError(usage);
|
|
1121
1121
|
data = await readArtifactCapability(home, taskId, {
|
|
1122
1122
|
relativePath, ...(commit === undefined ? {} : { commit })
|
|
1123
|
-
});
|
|
1123
|
+
}, () => store.getConfig());
|
|
1124
1124
|
}
|
|
1125
1125
|
else {
|
|
1126
1126
|
// save: a delivery-authoritative action; a managed Task caller must be the current Leader.
|
|
@@ -1156,7 +1156,7 @@ export async function main() {
|
|
|
1156
1156
|
relativePath, content, ...(encoding === undefined ? {} : { encoding }),
|
|
1157
1157
|
...(message === undefined ? {} : { message }),
|
|
1158
1158
|
...(expectedHead === undefined ? {} : { expectedHead })
|
|
1159
|
-
});
|
|
1159
|
+
}, () => store.getConfig());
|
|
1160
1160
|
}
|
|
1161
1161
|
emit(JSON.stringify(data, null, 2), false, data);
|
|
1162
1162
|
return;
|
|
@@ -2515,7 +2515,7 @@ async function deltaRecheckPreflightForTaskCommand(args, store, actualTaskReview
|
|
|
2515
2515
|
repositoryPaths,
|
|
2516
2516
|
previousRound: previous,
|
|
2517
2517
|
candidate: actualTaskReviewCandidate,
|
|
2518
|
-
git: new NodeGitWorkspace()
|
|
2518
|
+
git: new NodeGitWorkspace(() => store.getConfig())
|
|
2519
2519
|
});
|
|
2520
2520
|
if (assessment.kind === "ineligible") {
|
|
2521
2521
|
throw usageError(`Delta-recheck is technically unavailable: ${assessment.reason}`);
|
|
@@ -59,21 +59,31 @@ export async function assertFileTaskControllerStorageCompatible(home, options =
|
|
|
59
59
|
export async function ensureFileTaskController(home, options = {}) {
|
|
60
60
|
const call = options.call ?? callController;
|
|
61
61
|
await waitForForeignHandover(home, options);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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);
|
|
70
82
|
}
|
|
71
83
|
// A handover may have started after the first lock check and before the
|
|
72
84
|
// failed discovery/socket call. Re-check before spawning so a managed
|
|
73
85
|
// Session can never resurrect the old Controller during an update.
|
|
74
86
|
await waitForForeignHandover(home, options);
|
|
75
|
-
const timeoutMs = positive(options.startupTimeoutMs, STARTUP_TIMEOUT_MS, "startupTimeoutMs");
|
|
76
|
-
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
77
87
|
const spawnController = options.spawnController ?? spawnDetachedFileTaskController;
|
|
78
88
|
// A readiness timeout is not exit evidence. Reuse the observable startup
|
|
79
89
|
// process for this exact Home rather than multiplying unready Controllers.
|
|
@@ -213,17 +223,26 @@ async function waitForControllerProcessExit(controller, options) {
|
|
|
213
223
|
const timeoutMs = positive(options.shutdownTimeoutMs, CONTROLLER_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
|
|
214
224
|
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
215
225
|
const deadline = Date.now() + timeoutMs;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const observed = readLinuxProcessIdentity(controller.pid);
|
|
221
|
-
// Preserve the existing physical-stop boundary: an exact zombie has
|
|
222
|
-
// exited, even if its parent has not reaped the process table entry.
|
|
223
|
-
if (observed?.startIdentity === controller.processStartIdentity && observed.state === "Z")
|
|
226
|
+
for (;;) {
|
|
227
|
+
let observationError;
|
|
228
|
+
try {
|
|
229
|
+
if (!processGenerationIsLive(controller.pid, controller.processStartIdentity))
|
|
224
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;
|
|
225
242
|
}
|
|
226
243
|
if (Date.now() >= deadline) {
|
|
244
|
+
if (observationError !== undefined)
|
|
245
|
+
throw observationError;
|
|
227
246
|
throw new ControllerClientError("CONTROLLER_TIMEOUT", `Controller process ${controller.pid} did not exit within ${timeoutMs} ms.`);
|
|
228
247
|
}
|
|
229
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;
|
|
@@ -842,6 +850,7 @@ export class FileTaskController {
|
|
|
842
850
|
return;
|
|
843
851
|
if (this.#stopped)
|
|
844
852
|
throw new Error("Controller runtime is stopped.");
|
|
853
|
+
this.#lifecycleChecks?.notify("startup");
|
|
845
854
|
this.#requestBackgroundPump();
|
|
846
855
|
this.#requestRuntimeObservation();
|
|
847
856
|
// Arm the Operator lane once for work that was already pending when the
|
|
@@ -862,6 +871,7 @@ export class FileTaskController {
|
|
|
862
871
|
}
|
|
863
872
|
stop() {
|
|
864
873
|
this.#stopped = true;
|
|
874
|
+
this.#lifecycleChecks?.close();
|
|
865
875
|
this.#workspaceWaitAbort.abort(new Error("Controller stopped while waiting for Project maintenance."));
|
|
866
876
|
this.#signalScheduler.stop();
|
|
867
877
|
this.#operatorSignalScheduler.stop();
|
|
@@ -905,6 +915,7 @@ export class FileTaskController {
|
|
|
905
915
|
if (this.#stopped)
|
|
906
916
|
throw new Error("Controller runtime is stopped.");
|
|
907
917
|
parseMailboxKey(key);
|
|
918
|
+
this.#lifecycleChecks?.notify("signal");
|
|
908
919
|
if (key === "operator") {
|
|
909
920
|
// Main lane owns durable Hook folding; the Operator lane is the sole
|
|
910
921
|
// consumer of the Operator delivery mailbox.
|
|
@@ -1017,6 +1028,8 @@ export class FileTaskController {
|
|
|
1017
1028
|
}
|
|
1018
1029
|
}
|
|
1019
1030
|
const firstRuntimeDrain = await this.#phases.observe("runtime-drain", "home", () => this.#drainRuntimeEvents());
|
|
1031
|
+
if (scope.kind === "full")
|
|
1032
|
+
this.#lifecycleChecks?.notify("periodic");
|
|
1020
1033
|
if (scope.kind === "full" && this.#hostTerminalRecovery !== undefined) {
|
|
1021
1034
|
await this.#observeMaintenance("Host terminal recovery", this.#hostTerminalRecovery);
|
|
1022
1035
|
}
|
|
@@ -1258,6 +1271,8 @@ export class FileTaskController {
|
|
|
1258
1271
|
? await processor.drainAsync(this.#now())
|
|
1259
1272
|
: processor.drain(this.#now());
|
|
1260
1273
|
this.#lastRuntimeDrain = result;
|
|
1274
|
+
if (result.acknowledgedEventIds.length > 0)
|
|
1275
|
+
this.#lifecycleChecks?.notify("events-persisted");
|
|
1261
1276
|
this.#runtimeDrainPasses += 1;
|
|
1262
1277
|
const metrics = result.metrics ?? ZERO_DRAIN_METRICS;
|
|
1263
1278
|
this.#runtimeListedEvents += metrics.listedEventCount;
|
|
@@ -1723,6 +1723,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
1723
1723
|
const disposition = runtimeCleanupDisposition(mailbox);
|
|
1724
1724
|
if (mailbox === null || disposition === null)
|
|
1725
1725
|
return false;
|
|
1726
|
+
if (disposition === "upgrade-host")
|
|
1727
|
+
return false;
|
|
1726
1728
|
if (mailbox.processing !== null) {
|
|
1727
1729
|
if (disposition !== "replace-session" && !mailbox.processing.batch.reasons.every(isRuntimeCleanupReason)) {
|
|
1728
1730
|
return false;
|