@zq-silk/yui 0.2.0 → 0.4.2
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/ARCHITECTURE.md +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, join, resolve } from "node:path";
|
|
5
|
+
import { activeLiveRoleAgentSession } from "../executor/agentExecutor.js";
|
|
6
|
+
import { inspectStorageSchema } from "../storage/storageSchema.js";
|
|
7
|
+
import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
8
|
+
import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
|
|
9
|
+
import { TmuxManager, yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
10
|
+
import { readControllerDiscovery } from "../core/controllerClient.js";
|
|
11
|
+
import { CONTROLLER_DISCOVERY_PATH } from "../core/protocol.js";
|
|
12
|
+
import { controllerSocketPath } from "../core/controllerEndpoint.js";
|
|
13
|
+
import { buildControllerResourceInventory } from "./resourceInventory.js";
|
|
14
|
+
import { CONTROLLER_DOMAIN_PATH, EPHEMERAL_DOMAIN_GRACE_MS, ephemeralDomainFingerprint, readEphemeralDomainIdentity, readLinuxProcessStartIdentity } from "./domainIdentity.js";
|
|
15
|
+
export async function scanControllerResourceInventory(options) {
|
|
16
|
+
const currentHome = resolve(options.currentHome);
|
|
17
|
+
const environment = options.environment ?? process.env;
|
|
18
|
+
const observedAt = (options.now ?? (() => new Date()))();
|
|
19
|
+
const warnings = [];
|
|
20
|
+
const activeSockets = readActiveUnixSocketPaths(warnings);
|
|
21
|
+
const processes = listLinuxProcesses(warnings)
|
|
22
|
+
.filter(({ pid }) => pid !== process.pid);
|
|
23
|
+
const homes = new Set([currentHome]);
|
|
24
|
+
if (options.scope === "all") {
|
|
25
|
+
for (const process of processes) {
|
|
26
|
+
if (process.yuiHome !== undefined)
|
|
27
|
+
homes.add(process.yuiHome);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
31
|
+
const rawTmuxArtifacts = listSocketArtifacts(join(tmpdir(), `tmux-${uid}`), /^yui-[a-f0-9]{24}$/u, "tmux-socket", activeSockets);
|
|
32
|
+
const homeFacts = [];
|
|
33
|
+
const associatedArtifacts = new Set();
|
|
34
|
+
for (const home of [...homes].sort()) {
|
|
35
|
+
const matchingProcesses = processes.filter(({ yuiHome }) => yuiHome === home);
|
|
36
|
+
const state = loadHomeState(home, warnings, options);
|
|
37
|
+
const domain = inspectRuntimeDomain(home, matchingProcesses, state.roles, state.storageStatus, observedAt);
|
|
38
|
+
const tmuxSocketPath = join(tmpdir(), `tmux-${uid}`, yuiTmuxServerName(home));
|
|
39
|
+
const tmuxArtifact = rawTmuxArtifacts.find(({ path }) => path === tmuxSocketPath);
|
|
40
|
+
if (tmuxArtifact !== undefined)
|
|
41
|
+
associatedArtifacts.add(tmuxArtifact.path);
|
|
42
|
+
const panes = options.panes !== undefined
|
|
43
|
+
? options.panes
|
|
44
|
+
: tmuxArtifact?.active === true
|
|
45
|
+
? inspectHomePanes(home, environment.YUI_TMUX_BIN ?? "tmux", warnings)
|
|
46
|
+
: [];
|
|
47
|
+
const discovery = await inspectDiscovery(home, matchingProcesses, activeSockets);
|
|
48
|
+
if (discovery.status === "valid"
|
|
49
|
+
&& !discovery.socketActive
|
|
50
|
+
&& matchingProcesses.some((candidate) => (candidate.kind === "controller"
|
|
51
|
+
&& candidate.pid === discovery.pid
|
|
52
|
+
&& candidate.startIdentity === discovery.processStartIdentity))) {
|
|
53
|
+
warnings.push(`Controller socket is not reachable for ${home}.`);
|
|
54
|
+
}
|
|
55
|
+
const artifacts = [];
|
|
56
|
+
if (discovery.status === "invalid" && discovery.artifact !== undefined) {
|
|
57
|
+
associatedArtifacts.add(discovery.artifact.path);
|
|
58
|
+
}
|
|
59
|
+
// Keep active sockets visible as report-only resources too. An expired
|
|
60
|
+
// domain must not self-close while its tmux namespace is still active;
|
|
61
|
+
// the next bounded pass can reclassify the exact socket after the pane and
|
|
62
|
+
// server converge.
|
|
63
|
+
if (tmuxArtifact !== undefined)
|
|
64
|
+
artifacts.push(tmuxArtifact);
|
|
65
|
+
const domainPath = join(home, CONTROLLER_DOMAIN_PATH);
|
|
66
|
+
const domainIdentity = readEphemeralDomainIdentity(home);
|
|
67
|
+
if (domainIdentity.status !== "absent" && existsSync(domainPath)) {
|
|
68
|
+
artifacts.push(fileArtifact(domainPath, "domain-identity", domain?.liveness === "active"));
|
|
69
|
+
}
|
|
70
|
+
const validDiscoveryProcess = discovery.status === "valid"
|
|
71
|
+
&& (matchingProcesses.some((candidate) => (candidate.pid === discovery.pid
|
|
72
|
+
&& candidate.startIdentity === discovery.processStartIdentity))
|
|
73
|
+
// The scanner omits its own process from signalable resources. A
|
|
74
|
+
// Controller nevertheless must recognize its exact discovery fence;
|
|
75
|
+
// otherwise its own expired-domain pass would delete controller.json
|
|
76
|
+
// and strand the still-running server without a callable endpoint.
|
|
77
|
+
|| (discovery.pid === process.pid
|
|
78
|
+
&& readLinuxProcessStartIdentity(process.pid) === discovery.processStartIdentity));
|
|
79
|
+
const discoveryPath = join(home, CONTROLLER_DISCOVERY_PATH);
|
|
80
|
+
const expectedControllerSocketPath = controllerSocketPath(home);
|
|
81
|
+
if (discovery.status === "valid"
|
|
82
|
+
&& !validDiscoveryProcess
|
|
83
|
+
&& existsSync(discoveryPath)) {
|
|
84
|
+
artifacts.push(fileArtifact(discoveryPath, "controller-discovery", false));
|
|
85
|
+
}
|
|
86
|
+
if (existsSync(expectedControllerSocketPath)
|
|
87
|
+
&& !activeSockets.has(expectedControllerSocketPath)
|
|
88
|
+
&& !validDiscoveryProcess) {
|
|
89
|
+
artifacts.push(fileArtifact(expectedControllerSocketPath, "controller-socket", false));
|
|
90
|
+
}
|
|
91
|
+
homeFacts.push({
|
|
92
|
+
yuiHome: home,
|
|
93
|
+
exists: existsSync(home),
|
|
94
|
+
storageStatus: state.storageStatus,
|
|
95
|
+
discovery,
|
|
96
|
+
panes,
|
|
97
|
+
roles: state.roles,
|
|
98
|
+
artifacts,
|
|
99
|
+
...(domain === undefined ? {} : { domain })
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const globalArtifacts = options.scope === "all"
|
|
103
|
+
? rawTmuxArtifacts.filter(({ path }) => (!associatedArtifacts.has(path)))
|
|
104
|
+
: [];
|
|
105
|
+
return buildControllerResourceInventory({
|
|
106
|
+
schemaVersion: 1,
|
|
107
|
+
observedAt: observedAt.toISOString(),
|
|
108
|
+
currentHome,
|
|
109
|
+
scope: options.scope,
|
|
110
|
+
processes: options.scope === "all"
|
|
111
|
+
? processes
|
|
112
|
+
: processes.filter(({ yuiHome }) => yuiHome === currentHome),
|
|
113
|
+
homes: homeFacts,
|
|
114
|
+
globalArtifacts,
|
|
115
|
+
warnings
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export function parseLinuxProcessStat(stat, clockTicks, systemUptimeMs) {
|
|
119
|
+
const closing = stat.lastIndexOf(")");
|
|
120
|
+
if (closing < 0)
|
|
121
|
+
throw new Error("Linux process stat command is invalid.");
|
|
122
|
+
const fields = stat.slice(closing + 1).trim().split(/\s+/u);
|
|
123
|
+
const ppid = Number(fields[1]);
|
|
124
|
+
const userTicks = Number(fields[11]);
|
|
125
|
+
const systemTicks = Number(fields[12]);
|
|
126
|
+
const startIdentity = fields[19];
|
|
127
|
+
if (!Number.isSafeInteger(ppid)
|
|
128
|
+
|| ppid < 0
|
|
129
|
+
|| !Number.isFinite(userTicks)
|
|
130
|
+
|| userTicks < 0
|
|
131
|
+
|| !Number.isFinite(systemTicks)
|
|
132
|
+
|| systemTicks < 0
|
|
133
|
+
|| startIdentity === undefined
|
|
134
|
+
|| !/^[0-9]{1,32}$/u.test(startIdentity)
|
|
135
|
+
|| !Number.isFinite(clockTicks)
|
|
136
|
+
|| clockTicks <= 0) {
|
|
137
|
+
throw new Error("Linux process stat fields are invalid.");
|
|
138
|
+
}
|
|
139
|
+
const startTicks = Number(startIdentity);
|
|
140
|
+
return {
|
|
141
|
+
ppid,
|
|
142
|
+
startIdentity,
|
|
143
|
+
cpuTimeMs: Math.max(0, Math.round((userTicks + systemTicks) * 1000 / clockTicks)),
|
|
144
|
+
ageMs: Math.max(0, Math.round(systemUptimeMs - startTicks * 1000 / clockTicks))
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function classifyRuntimeProcess(args, command) {
|
|
148
|
+
if (args.some((argument) => /(?:^|\/)controllerMain\.js$/u.test(argument))) {
|
|
149
|
+
return "controller";
|
|
150
|
+
}
|
|
151
|
+
if (args.includes("app-server"))
|
|
152
|
+
return "app-server";
|
|
153
|
+
if (command.startsWith("tmux: server")
|
|
154
|
+
|| args.some((argument, index) => (argument === "-L"
|
|
155
|
+
&& /^yui-[a-f0-9]{24}$/u.test(args[index + 1] ?? "")))) {
|
|
156
|
+
return "tmux-server";
|
|
157
|
+
}
|
|
158
|
+
const executable = basename(args[0] ?? command);
|
|
159
|
+
if (args.includes("web")
|
|
160
|
+
&& args.some((argument) => /(?:^|\/)(?:cli|yui)(?:\.js)?$/u.test(argument))) {
|
|
161
|
+
return "web";
|
|
162
|
+
}
|
|
163
|
+
if (executable === "codex" || executable === "claude")
|
|
164
|
+
return "agent";
|
|
165
|
+
return "other";
|
|
166
|
+
}
|
|
167
|
+
function listLinuxProcesses(warnings) {
|
|
168
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
169
|
+
const clockTicks = readClockTicks();
|
|
170
|
+
const uptimeMs = readSystemUptimeMs();
|
|
171
|
+
let entries;
|
|
172
|
+
try {
|
|
173
|
+
// Request names only. On some Node/filesystem combinations, Dirent
|
|
174
|
+
// construction may lstat unknown /proc entries while readdir is still in
|
|
175
|
+
// progress; a process exiting in that window aborts the whole inventory.
|
|
176
|
+
entries = readdirSync("/proc", { encoding: "utf8" });
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
warnings.push(`Cannot enumerate Linux processes: ${message(error)}`);
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
return entries.flatMap((entryName) => {
|
|
183
|
+
try {
|
|
184
|
+
const pid = linuxProcessEntryPid(entryName);
|
|
185
|
+
if (pid === undefined)
|
|
186
|
+
return [];
|
|
187
|
+
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
188
|
+
const processUid = parseStatusNumber(status, "Uid");
|
|
189
|
+
if (processUid !== uid)
|
|
190
|
+
return [];
|
|
191
|
+
const parsed = parseLinuxProcessStat(readFileSync(`/proc/${pid}/stat`, "utf8"), clockTicks, uptimeMs);
|
|
192
|
+
const args = splitNullDelimited(readFileSync(`/proc/${pid}/cmdline`));
|
|
193
|
+
const command = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
|
|
194
|
+
const yuiHome = readYuiHome(pid);
|
|
195
|
+
const io = readProcessIo(pid);
|
|
196
|
+
return [{
|
|
197
|
+
pid,
|
|
198
|
+
ppid: parsed.ppid,
|
|
199
|
+
uid: processUid,
|
|
200
|
+
startIdentity: parsed.startIdentity,
|
|
201
|
+
...(yuiHome === undefined ? {} : { yuiHome }),
|
|
202
|
+
kind: classifyRuntimeProcess(args, command),
|
|
203
|
+
command,
|
|
204
|
+
args,
|
|
205
|
+
rssBytes: parseStatusNumber(status, "VmRSS", 0) * 1024,
|
|
206
|
+
cpuTimeMs: parsed.cpuTimeMs,
|
|
207
|
+
...(io === undefined ? {} : io),
|
|
208
|
+
ageMs: parsed.ageMs
|
|
209
|
+
}];
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Processes commonly exit during a /proc scan. A point-in-time inventory
|
|
213
|
+
// omits those races instead of turning them into persistent warnings.
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Resolve one numeric /proc entry name without asking the filesystem for
|
|
220
|
+
* entry metadata. Its process files are then read inside the per-PID race
|
|
221
|
+
* boundary above.
|
|
222
|
+
*/
|
|
223
|
+
export function linuxProcessEntryPid(entryName) {
|
|
224
|
+
if (!/^[1-9][0-9]*$/u.test(entryName))
|
|
225
|
+
return undefined;
|
|
226
|
+
const pid = Number(entryName);
|
|
227
|
+
return Number.isSafeInteger(pid) ? pid : undefined;
|
|
228
|
+
}
|
|
229
|
+
function readYuiHome(pid) {
|
|
230
|
+
const environment = splitNullDelimited(readFileSync(`/proc/${pid}/environ`));
|
|
231
|
+
const entry = environment.find((value) => value.startsWith("YUI_HOME="));
|
|
232
|
+
const value = entry?.slice("YUI_HOME=".length);
|
|
233
|
+
return value === undefined || value.length === 0 ? undefined : resolve(value);
|
|
234
|
+
}
|
|
235
|
+
function readProcessIo(pid) {
|
|
236
|
+
try {
|
|
237
|
+
const contents = readFileSync(`/proc/${pid}/io`, "utf8");
|
|
238
|
+
const readBytes = parseOptionalIoCounter(contents, "read_bytes");
|
|
239
|
+
const writeBytes = parseOptionalIoCounter(contents, "write_bytes");
|
|
240
|
+
return readBytes === undefined || writeBytes === undefined
|
|
241
|
+
? undefined
|
|
242
|
+
: { ioReadBytes: readBytes, ioWriteBytes: writeBytes };
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// /proc/<pid>/io can disappear or be restricted during a point-in-time scan.
|
|
246
|
+
// CPU/RSS inventory remains useful, but no IO activity is inferred.
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function parseOptionalIoCounter(contents, label) {
|
|
251
|
+
const match = new RegExp(`^${label}:\\s+([0-9]+)`, "mu").exec(contents);
|
|
252
|
+
if (match === null)
|
|
253
|
+
return undefined;
|
|
254
|
+
const value = Number(match[1]);
|
|
255
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
256
|
+
}
|
|
257
|
+
function splitNullDelimited(value) {
|
|
258
|
+
return value.toString("utf8").split("\0").filter((entry) => entry.length > 0);
|
|
259
|
+
}
|
|
260
|
+
function parseStatusNumber(status, label, fallback) {
|
|
261
|
+
const match = new RegExp(`^${label}:\\s+([0-9]+)`, "mu").exec(status);
|
|
262
|
+
if (match === null) {
|
|
263
|
+
if (fallback !== undefined)
|
|
264
|
+
return fallback;
|
|
265
|
+
throw new Error(`Linux process ${label} is unavailable.`);
|
|
266
|
+
}
|
|
267
|
+
const value = Number(match[1]);
|
|
268
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
269
|
+
throw new Error(`Linux process ${label} is invalid.`);
|
|
270
|
+
}
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
function readClockTicks() {
|
|
274
|
+
const result = spawnSync("getconf", ["CLK_TCK"], {
|
|
275
|
+
encoding: "utf8",
|
|
276
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
277
|
+
});
|
|
278
|
+
const value = Number(result.stdout?.trim());
|
|
279
|
+
return Number.isSafeInteger(value) && value > 0 ? value : 100;
|
|
280
|
+
}
|
|
281
|
+
function readSystemUptimeMs() {
|
|
282
|
+
const seconds = Number(readFileSync("/proc/uptime", "utf8").split(/\s+/u)[0]);
|
|
283
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
284
|
+
throw new Error("Linux system uptime is invalid.");
|
|
285
|
+
}
|
|
286
|
+
return seconds * 1000;
|
|
287
|
+
}
|
|
288
|
+
function readActiveUnixSocketPaths(warnings) {
|
|
289
|
+
try {
|
|
290
|
+
const lines = readFileSync("/proc/net/unix", "utf8").split("\n").slice(1);
|
|
291
|
+
return new Set(lines.flatMap((line) => {
|
|
292
|
+
const fields = line.trim().split(/\s+/u);
|
|
293
|
+
const path = fields[7];
|
|
294
|
+
return path === undefined || !path.startsWith("/") ? [] : [path];
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
warnings.push(`Cannot inspect Unix sockets: ${message(error)}`);
|
|
299
|
+
return new Set();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function listSocketArtifacts(directory, pattern, artifactKind, activeSockets) {
|
|
303
|
+
try {
|
|
304
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
305
|
+
if (!pattern.test(entry.name))
|
|
306
|
+
return [];
|
|
307
|
+
const path = join(directory, entry.name);
|
|
308
|
+
let metadata;
|
|
309
|
+
try {
|
|
310
|
+
metadata = lstatSync(path);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
if (!metadata.isSocket())
|
|
316
|
+
return [];
|
|
317
|
+
return [{
|
|
318
|
+
artifactKind,
|
|
319
|
+
path,
|
|
320
|
+
active: activeSockets.has(path),
|
|
321
|
+
fingerprint: statFingerprint(metadata)
|
|
322
|
+
}];
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async function inspectDiscovery(home, processes, activeSockets) {
|
|
330
|
+
const path = join(home, CONTROLLER_DISCOVERY_PATH);
|
|
331
|
+
try {
|
|
332
|
+
const discovery = await readControllerDiscovery(home);
|
|
333
|
+
return {
|
|
334
|
+
status: "valid",
|
|
335
|
+
pid: discovery.pid,
|
|
336
|
+
processStartIdentity: discovery.processStartIdentity,
|
|
337
|
+
socketPath: discovery.socketPath,
|
|
338
|
+
socketActive: activeSockets.has(discovery.socketPath),
|
|
339
|
+
fingerprint: existsSync(path)
|
|
340
|
+
? statFingerprint(lstatSync(path))
|
|
341
|
+
: `${discovery.pid}:${discovery.processStartIdentity}`
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
if (!existsSync(path))
|
|
346
|
+
return { status: "absent" };
|
|
347
|
+
const artifact = fileArtifact(path, "controller-discovery", false);
|
|
348
|
+
const possibleOwner = processes.some(({ kind }) => kind === "controller");
|
|
349
|
+
return possibleOwner
|
|
350
|
+
? { status: "invalid", artifact: { ...artifact, active: true } }
|
|
351
|
+
: { status: "invalid", artifact };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function inspectRuntimeDomain(home, processes, roles, storageStatus, now) {
|
|
355
|
+
const stored = readEphemeralDomainIdentity(home);
|
|
356
|
+
if (stored.status === "absent") {
|
|
357
|
+
// A current, ordinary YUI_HOME is a real control domain, not a disposable
|
|
358
|
+
// test domain. Keep it visible even when it is currently idle so a stale
|
|
359
|
+
// artifact cannot acquire ephemeral cleanup authority by omission. An
|
|
360
|
+
// uninitialized temporary directory with no runtime facts remains absent
|
|
361
|
+
// to preserve the legacy empty-home inventory behavior.
|
|
362
|
+
if (storageStatus !== "current"
|
|
363
|
+
&& processes.length === 0
|
|
364
|
+
&& roles.length === 0)
|
|
365
|
+
return undefined;
|
|
366
|
+
return {
|
|
367
|
+
kind: "unmarked",
|
|
368
|
+
liveness: "unknown",
|
|
369
|
+
disposition: "review",
|
|
370
|
+
reasonCode: "unmarked-domain",
|
|
371
|
+
fingerprint: `unmarked:${home}`,
|
|
372
|
+
tmuxTargets: [],
|
|
373
|
+
ageMs: 0,
|
|
374
|
+
graceMs: EPHEMERAL_DOMAIN_GRACE_MS
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (stored.status === "invalid" || stored.identity === undefined) {
|
|
378
|
+
return {
|
|
379
|
+
kind: "invalid",
|
|
380
|
+
liveness: "unknown",
|
|
381
|
+
disposition: "review",
|
|
382
|
+
reasonCode: "invalid-domain-identity",
|
|
383
|
+
fingerprint: `invalid:${stored.fingerprint ?? home}`,
|
|
384
|
+
tmuxTargets: [],
|
|
385
|
+
ageMs: 0,
|
|
386
|
+
graceMs: EPHEMERAL_DOMAIN_GRACE_MS
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const identity = stored.identity;
|
|
390
|
+
const ageMs = Math.max(0, now.getTime() - Date.parse(identity.createdAt));
|
|
391
|
+
if (identity.tmuxServer !== yuiTmuxServerName(home)) {
|
|
392
|
+
return {
|
|
393
|
+
kind: "invalid",
|
|
394
|
+
liveness: "unknown",
|
|
395
|
+
disposition: "review",
|
|
396
|
+
reasonCode: "invalid-domain-tmux-server",
|
|
397
|
+
fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
|
|
398
|
+
hostPid: identity.hostPid,
|
|
399
|
+
hostProcessStartIdentity: identity.hostProcessStartIdentity,
|
|
400
|
+
token: identity.token,
|
|
401
|
+
tmuxServer: identity.tmuxServer,
|
|
402
|
+
tmuxTargets: identity.tmuxTargets,
|
|
403
|
+
createdAt: identity.createdAt,
|
|
404
|
+
ageMs,
|
|
405
|
+
graceMs: EPHEMERAL_DOMAIN_GRACE_MS
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const hostStartIdentity = readLinuxProcessStartIdentity(identity.hostPid);
|
|
409
|
+
const hostPathExists = existsSync(`/proc/${identity.hostPid}`);
|
|
410
|
+
const hostActive = hostStartIdentity === identity.hostProcessStartIdentity;
|
|
411
|
+
const storageSafe = storageStatus === "current";
|
|
412
|
+
if (hostActive) {
|
|
413
|
+
return {
|
|
414
|
+
kind: "ephemeral-test",
|
|
415
|
+
liveness: "active",
|
|
416
|
+
disposition: "protected",
|
|
417
|
+
reasonCode: "ephemeral-host-active",
|
|
418
|
+
fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
|
|
419
|
+
hostPid: identity.hostPid,
|
|
420
|
+
hostProcessStartIdentity: identity.hostProcessStartIdentity,
|
|
421
|
+
token: identity.token,
|
|
422
|
+
tmuxServer: identity.tmuxServer,
|
|
423
|
+
tmuxTargets: identity.tmuxTargets,
|
|
424
|
+
createdAt: identity.createdAt,
|
|
425
|
+
ageMs,
|
|
426
|
+
graceMs: EPHEMERAL_DOMAIN_GRACE_MS
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const activeRole = roles.some((role) => ((role.ownerKind === "task-role" && role.taskStatus === "active")
|
|
430
|
+
// Global Roles have no Task status. Only a live native Session is a
|
|
431
|
+
// durable liveness fact for that scope; stopped/broken history must not
|
|
432
|
+
// protect an expired disposable domain merely because it has an ID.
|
|
433
|
+
|| (role.ownerKind === "global-role" && role.nativeSessionId !== undefined)));
|
|
434
|
+
if (!storageSafe) {
|
|
435
|
+
return {
|
|
436
|
+
kind: "ephemeral-test",
|
|
437
|
+
liveness: hostActive ? "active" : "expired",
|
|
438
|
+
disposition: "review",
|
|
439
|
+
reasonCode: `ephemeral-storage-${storageStatus}`,
|
|
440
|
+
fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
|
|
441
|
+
hostPid: identity.hostPid,
|
|
442
|
+
hostProcessStartIdentity: identity.hostProcessStartIdentity,
|
|
443
|
+
token: identity.token,
|
|
444
|
+
tmuxServer: identity.tmuxServer,
|
|
445
|
+
tmuxTargets: identity.tmuxTargets,
|
|
446
|
+
createdAt: identity.createdAt,
|
|
447
|
+
ageMs,
|
|
448
|
+
graceMs: EPHEMERAL_DOMAIN_GRACE_MS
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (activeRole) {
|
|
452
|
+
return {
|
|
453
|
+
kind: "ephemeral-test",
|
|
454
|
+
liveness: "expired",
|
|
455
|
+
disposition: "protected",
|
|
456
|
+
reasonCode: "ephemeral-active-task-role",
|
|
457
|
+
fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
|
|
458
|
+
hostPid: identity.hostPid,
|
|
459
|
+
hostProcessStartIdentity: identity.hostProcessStartIdentity,
|
|
460
|
+
token: identity.token,
|
|
461
|
+
tmuxServer: identity.tmuxServer,
|
|
462
|
+
tmuxTargets: identity.tmuxTargets,
|
|
463
|
+
createdAt: identity.createdAt,
|
|
464
|
+
ageMs,
|
|
465
|
+
graceMs: EPHEMERAL_DOMAIN_GRACE_MS
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
if (hostStartIdentity === undefined && hostPathExists) {
|
|
469
|
+
return {
|
|
470
|
+
kind: "ephemeral-test",
|
|
471
|
+
liveness: "expired",
|
|
472
|
+
disposition: "review",
|
|
473
|
+
reasonCode: "ephemeral-host-identity-unavailable",
|
|
474
|
+
fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
|
|
475
|
+
hostPid: identity.hostPid,
|
|
476
|
+
hostProcessStartIdentity: identity.hostProcessStartIdentity,
|
|
477
|
+
token: identity.token,
|
|
478
|
+
tmuxServer: identity.tmuxServer,
|
|
479
|
+
tmuxTargets: identity.tmuxTargets,
|
|
480
|
+
createdAt: identity.createdAt,
|
|
481
|
+
ageMs,
|
|
482
|
+
graceMs: EPHEMERAL_DOMAIN_GRACE_MS
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (ageMs < EPHEMERAL_DOMAIN_GRACE_MS) {
|
|
486
|
+
return {
|
|
487
|
+
kind: "ephemeral-test",
|
|
488
|
+
liveness: "expired",
|
|
489
|
+
disposition: "review",
|
|
490
|
+
reasonCode: hostStartIdentity === undefined
|
|
491
|
+
? "ephemeral-host-identity-unavailable"
|
|
492
|
+
: "ephemeral-host-grace",
|
|
493
|
+
fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
|
|
494
|
+
hostPid: identity.hostPid,
|
|
495
|
+
hostProcessStartIdentity: identity.hostProcessStartIdentity,
|
|
496
|
+
token: identity.token,
|
|
497
|
+
tmuxServer: identity.tmuxServer,
|
|
498
|
+
tmuxTargets: identity.tmuxTargets,
|
|
499
|
+
createdAt: identity.createdAt,
|
|
500
|
+
ageMs,
|
|
501
|
+
graceMs: EPHEMERAL_DOMAIN_GRACE_MS
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
kind: "ephemeral-test",
|
|
506
|
+
liveness: "expired",
|
|
507
|
+
disposition: "safe",
|
|
508
|
+
reasonCode: hostStartIdentity === undefined
|
|
509
|
+
? "ephemeral-host-dead"
|
|
510
|
+
: "ephemeral-host-identity-mismatch",
|
|
511
|
+
fingerprint: ephemeralDomainFingerprint(identity, stored.fingerprint),
|
|
512
|
+
hostPid: identity.hostPid,
|
|
513
|
+
hostProcessStartIdentity: identity.hostProcessStartIdentity,
|
|
514
|
+
token: identity.token,
|
|
515
|
+
tmuxServer: identity.tmuxServer,
|
|
516
|
+
tmuxTargets: identity.tmuxTargets,
|
|
517
|
+
createdAt: identity.createdAt,
|
|
518
|
+
ageMs,
|
|
519
|
+
graceMs: EPHEMERAL_DOMAIN_GRACE_MS
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function loadHomeState(home, warnings, options) {
|
|
523
|
+
const schema = (options.inspectStorage ?? inspectStorageSchema)(home);
|
|
524
|
+
const recordOnlyOlder = schema.status === "unsupported"
|
|
525
|
+
&& schema.incompatibleComponent === "record"
|
|
526
|
+
&& schema.direction === "older"
|
|
527
|
+
&& schema.currentLayoutVersion === schema.latestLayoutVersion
|
|
528
|
+
&& schema.currentAggregateSchemaVersion === schema.latestAggregateSchemaVersion;
|
|
529
|
+
if (schema.status !== "current" && !recordOnlyOlder) {
|
|
530
|
+
return {
|
|
531
|
+
storageStatus: schema.status,
|
|
532
|
+
roles: []
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const store = (options.openCompatibleStore ?? openCompatibleFileTaskStore)(home);
|
|
537
|
+
const roles = store.listGlobalRoles().map((role) => {
|
|
538
|
+
const session = activeLiveRoleAgentSession(store.getGlobalRoleSessionSet(role.name));
|
|
539
|
+
const binding = role.agentBindings[role.activeAgentId];
|
|
540
|
+
const agentId = session?.effective.agentId ?? role.activeAgentId;
|
|
541
|
+
const adapterId = session?.effective.adapterId ?? binding?.adapterId;
|
|
542
|
+
return {
|
|
543
|
+
ownerKind: "global-role",
|
|
544
|
+
roleName: role.name,
|
|
545
|
+
agentId,
|
|
546
|
+
...(adapterId === undefined ? {} : { adapterId }),
|
|
547
|
+
...(session === null ? {} : { nativeSessionId: session.nativeSessionId }),
|
|
548
|
+
...(session?.launchId === undefined ? {} : { launchId: session.launchId })
|
|
549
|
+
};
|
|
550
|
+
});
|
|
551
|
+
for (const task of store.listTasks()) {
|
|
552
|
+
for (const role of store.listRoles(task.id)) {
|
|
553
|
+
const session = activeLiveRoleAgentSession(store.getRoleSessionSet(task.id, role.name));
|
|
554
|
+
const binding = role.agentBindings[role.activeAgentId];
|
|
555
|
+
const run = store.getActiveAgentRun(task.id, role.name);
|
|
556
|
+
const agentId = session?.effective.agentId ?? role.activeAgentId;
|
|
557
|
+
const adapterId = session?.effective.adapterId ?? binding?.adapterId;
|
|
558
|
+
roles.push({
|
|
559
|
+
ownerKind: "task-role",
|
|
560
|
+
taskId: task.id,
|
|
561
|
+
taskTitle: task.title,
|
|
562
|
+
taskStatus: task.status,
|
|
563
|
+
roleName: role.name,
|
|
564
|
+
agentId,
|
|
565
|
+
...(adapterId === undefined ? {} : { adapterId }),
|
|
566
|
+
...(session === null ? {} : { nativeSessionId: session.nativeSessionId }),
|
|
567
|
+
...(session?.launchId === undefined ? {} : { launchId: session.launchId }),
|
|
568
|
+
...(run === null ? {} : { runId: run.id })
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return { storageStatus: schema.status, roles };
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
warnings.push(`Cannot load runtime ownership for ${home}: ${message(error)}`);
|
|
576
|
+
return { storageStatus: "invalid", roles: [] };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function inspectHomePanes(home, tmuxBin, warnings) {
|
|
580
|
+
try {
|
|
581
|
+
return new TmuxManager(tmuxBin, new NodeCommandExecutor(), {
|
|
582
|
+
yuiHome: home
|
|
583
|
+
}).inspectRolePaneInventory();
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
warnings.push(`Cannot inspect tmux for ${home}: ${message(error)}`);
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function fileArtifact(path, artifactKind, active) {
|
|
591
|
+
const metadata = lstatSync(path);
|
|
592
|
+
return {
|
|
593
|
+
artifactKind,
|
|
594
|
+
path,
|
|
595
|
+
active,
|
|
596
|
+
fingerprint: statFingerprint(metadata)
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function statFingerprint(metadata) {
|
|
600
|
+
return [
|
|
601
|
+
metadata.dev,
|
|
602
|
+
metadata.ino,
|
|
603
|
+
metadata.mode,
|
|
604
|
+
metadata.size,
|
|
605
|
+
Math.trunc(metadata.mtimeMs)
|
|
606
|
+
].join(":");
|
|
607
|
+
}
|
|
608
|
+
function message(error) {
|
|
609
|
+
return error instanceof Error ? error.message : String(error);
|
|
610
|
+
}
|